Can not find a (Map) Key deserializer for type [simple type, class ...]

JavaJsonJacksonDeserialization

Java Problem Overview


I have a domain object that has a Map:

private Map<AutoHandlingSlotKey, LinkedHashSet<AutoFunction>> autoHandling;

When I serialize the object, I get this:

"autoHandling" : [ "java.util.HashMap", {
} ],

This Map's key is a custom Object:

public class AutoHandlingSlotKey implements Serializable {
    private FunctionalArea slot; // ENUM
    private String returnView;   // ENUM

So, I am not sure how to correct this exception I keep getting when I deserialize the object:

org.codehaus.jackson.map.JsonMappingException: Can not find a (Map) Key deserializer for type [simple type, class com.comcast.ivr.core.domain.AutoHandlingSlotKey]

How to correct this issue? I do not have access to the domain object to modify.

Java Solutions


Solution 1 - Java

This was asked a long time ago, and is the first google result when looking up the error, but the accepted answer has no code and might be confusing for a jackson beginner (me). I eventually found this answer that helped.

So, as stated in accepted answer, Implementing and register a "key deserializer" is the way to go. You can do this like this.

SimpleModule simpleModule = new SimpleModule();
simpleModule.addKeyDeserializer(YourClass.class, new YourClassKeyDeserializer());
objectMapper.registerModule(simpleModule);

And for the class, all you have to do is:

class YourClassKeyDeserializer extends KeyDeserializer
{
    @Override
    public Object deserializeKey(final String key, final DeserializationContext ctxt ) throws IOException, JsonProcessingException
    {
        return null; // replace null with your logic
    }
}

That's it! No annotation on classes, not custom deserializer for maps, etc.

Solution 2 - Java

By default, Jackson tries to serialize Java Maps as JSON Objects (key/value pairs), so Map key object must be somehow serialized as a String; and there must be matching (and registered) key deserializer. Default configuration only supports a small set of JDK types (String, numbers, enum). So mapper has no idea as to how to take a String and create AutoHandlingSlotKey out of it. (in fact I am surprised that serializer did not fail for same reason)

Two obvious ways to solve this are:

  • Implement and register a "key deserializer"
  • Implement and register a custom deserializer for Maps.

In your case it is probably easier to do former. You may also want to implement custom key serializer, to ensure keys are serializer in proper format.

The easiest way to register serializers and deserializers is by Module interface that was added in Jackson 1.7 (and extended in 1.8 to support key serializers/deserializers).

Solution 3 - Java

Here is a generic Map serializer and deserializer that uses a list of key-value pairs, instead of JSON key-value pairs.

[
    {
        "key": Object,
        "value": Object
    }...
]

package default;

import java.io.IOException;
import java.util.List;
import java.util.Map;
import java.util.stream.Collectors;

import com.fasterxml.jackson.core.JsonGenerator;
import com.fasterxml.jackson.databind.SerializerProvider;
import com.fasterxml.jackson.databind.ser.impl.MapEntrySerializer;
import com.fasterxml.jackson.databind.ser.std.MapSerializer;
import com.fasterxml.jackson.databind.ser.std.StdSerializer;

/**
 * Simple Map Serializer<br>
 * <br>
 * Serializes the map as a list of key-value pairs, instead of as a list of JSON
 * key-value pairs (using the default serializer {@link MapSerializer}).
 *
 * @param <K> the type of keys maintained by the map
 * @param <V> the type of mapped values
 * @author Gitesh Agarwal (gagarwa)
 */
public class SimpleMapSerializer<K, V> extends StdSerializer<Map<K, V>> {

	private static final long serialVersionUID = 1L;

	/**
	 * Default Constructor
	 */
	public SimpleMapSerializer() {
		super(Map.class, true);
	}

	@Override
	public void serialize(Map<K, V> value, JsonGenerator gen, SerializerProvider provider) throws IOException {
		List<SimpleEntry<K, V>> listValues = value.entrySet()
				.stream()
				.map(SimpleEntry::new)
				.collect(Collectors.toList());

		provider.defaultSerializeValue(listValues, gen);
	}

	/**
	 * Simple Entry<br>
	 * <br>
	 * Intentionally does not implement the {@link Map.Entry} interface, so as not
	 * to invoke the default serializer {@link MapEntrySerializer}.
	 *
	 * @author Gitesh Agarwal (gagarwa)
	 */
	protected static class SimpleEntry<K, V> {

		private K key;

		private V value;

		/**
		 * Default Constructor
		 * 
		 * @param entry the map entry
		 */
		public SimpleEntry(Map.Entry<K, V> entry) {
			key = entry.getKey();
			value = entry.getValue();
		}

		/**
		 * @return the key
		 */
		public K getKey() {
			return key;
		}

		/**
		 * @return the value
		 */
		public V getValue() {
			return value;
		}

	}

}

If you don't want to define a custom serializer everytime.

package default;

import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;

import com.fasterxml.jackson.databind.BeanDescription;
import com.fasterxml.jackson.databind.JsonSerializer;
import com.fasterxml.jackson.databind.Module;
import com.fasterxml.jackson.databind.SerializationConfig;
import com.fasterxml.jackson.databind.module.SimpleModule;
import com.fasterxml.jackson.databind.ser.BeanSerializerModifier;
import com.fasterxml.jackson.databind.ser.std.StdKeySerializers;
import com.fasterxml.jackson.databind.type.MapType;
import com.ibm.response.SimpleMapSerializer;

/**
 * Map Serializer Modifier
 *
 * @author Gitesh Agarwal (gagarwa)
 */
@Configuration
public class MapSerializerModifier extends BeanSerializerModifier {

	@Override
	@SuppressWarnings("rawtypes")
	public JsonSerializer<?> modifyMapSerializer(SerializationConfig config, MapType valueType,
			BeanDescription beanDesc, JsonSerializer<?> serializer) {

		JsonSerializer keySerializer = StdKeySerializers.getStdKeySerializer(config,
				valueType.getKeyType().getRawClass(), false);

		if (keySerializer == null)
			return new SimpleMapSerializer();

		return serializer;
	}

	/**
	 * Simple Module Builder, including the map serializer modifier.
	 * 
	 * @return the module
	 */
	@Bean
	public Module module() {
		SimpleModule module = new SimpleModule();
		module.setSerializerModifier(new MapSerializerModifier());
		return module;
	}

}


The deserializer is a little more tricky, because you need to maintain type information for a generic version.

package default;

import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;

import com.fasterxml.jackson.databind.BeanDescription;
import com.fasterxml.jackson.databind.DeserializationConfig;
import com.fasterxml.jackson.databind.JsonDeserializer;
import com.fasterxml.jackson.databind.KeyDeserializer;
import com.fasterxml.jackson.databind.Module;
import com.fasterxml.jackson.databind.deser.BeanDeserializerModifier;
import com.fasterxml.jackson.databind.deser.std.StdKeyDeserializer;
import com.fasterxml.jackson.databind.module.SimpleModule;
import com.fasterxml.jackson.databind.type.MapType;
import com.ibm.request.action.SimpleMapDeserializer;

/**
 * Map Deserializer Modifier
 *
 * @author Gitesh Agarwal (gagarwa)
 */
@Configuration
public class MapDeserializerModifier extends BeanDeserializerModifier {

	@Override
	@SuppressWarnings("rawtypes")
	public JsonDeserializer<?> modifyMapDeserializer(DeserializationConfig config, MapType type,
			BeanDescription beanDesc, JsonDeserializer<?> deserializer) {

		KeyDeserializer keyDeserializer = StdKeyDeserializer.forType(type.getKeyType().getRawClass());

		if (keyDeserializer == null)
			return new SimpleMapDeserializer(type, config.getTypeFactory());

		return deserializer;
	}

	/**
	 * Simple Module Builder, including the map deserializer modifier.
	 * 
	 * @return the module
	 */
	@Bean
	public Module module() {
		SimpleModule module = new SimpleModule();
		module.setDeserializerModifier(new MapDeserializerModifier());
		return module;
	}

}

package default;

package com.ibm.request.action;

import java.io.IOException;
import java.util.HashMap;
import java.util.List;
import java.util.Map;

import com.fasterxml.jackson.core.JsonParser;
import com.fasterxml.jackson.core.JsonProcessingException;
import com.fasterxml.jackson.databind.DeserializationContext;
import com.fasterxml.jackson.databind.deser.std.MapDeserializer;
import com.fasterxml.jackson.databind.deser.std.MapEntryDeserializer;
import com.fasterxml.jackson.databind.deser.std.StdDeserializer;
import com.fasterxml.jackson.databind.type.CollectionType;
import com.fasterxml.jackson.databind.type.MapType;
import com.fasterxml.jackson.databind.type.TypeFactory;

/**
 * Simple Map Deserializer<br>
 * <br>
 * Deserializes the map from a list of key-value pairs, instead of from a list
 * of JSON key-value pairs (using the default deserializer
 * {@link MapDeserializer}).
 *
 * @param <K> the type of keys maintained by the map
 * @param <V> the type of mapped values
 * @author Gitesh Agarwal (gagarwa)
 */
public class SimpleMapDeserializer<K, V> extends StdDeserializer<Map<K, V>> {

	private static final long serialVersionUID = 1L;

	private final CollectionType type;

	/**
	 * Default Constructor
	 * 
	 * @param type    the map type (key, value)
	 * @param factory the type factory, to create the collection type
	 */
	public SimpleMapDeserializer(MapType type, TypeFactory factory) {
		super(Map.class);
		this.type = factory.constructCollectionType(List.class,
				factory.constructParametricType(SimpleEntry.class, type.getKeyType(), type.getContentType()));
	}

	@Override
	public Map<K, V> deserialize(JsonParser p, DeserializationContext ctxt)
			throws IOException, JsonProcessingException {

		List<SimpleEntry<K, V>> listValues = ctxt.readValue(p, type);
		HashMap<K, V> value = new HashMap<>();

		listValues.forEach(e -> value.put(e.key, e.value));
		return value;
	}

	/**
	 * Simple Entry<br>
	 * <br>
	 * Intentionally does not implement the {@link Map.Entry} interface, so as not
	 * to invoke the default deserializer {@link MapEntryDeserializer}.
	 *
	 * @author Gitesh Agarwal (gagarwa)
	 */
	protected static class SimpleEntry<K, V> {

		private K key;

		private V value;

		/**
		 * Default Constructor
		 */
		public SimpleEntry() {

		}

		/**
		 * @param key the key
		 */
		public void setKey(K key) {
			this.key = key;
		}

		/**
		 * @param value the value
		 */
		public void setValue(V value) {
			this.value = value;
		}

	}

}

Attributions

All content for this solution is sourced from the original question on Stackoverflow.

The content on this page is licensed under the Attribution-ShareAlike 4.0 International (CC BY-SA 4.0) license.

Content TypeOriginal AuthorOriginal Content on Stackoverflow
QuestionMick KnutsonView Question on Stackoverflow
Solution 1 - JavaestebanrvView Answer on Stackoverflow
Solution 2 - JavaStaxManView Answer on Stackoverflow
Solution 3 - JavagagarwaView Answer on Stackoverflow