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

Viewed 66782

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.

5 Answers

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;
        }

    }

}

Implementing a String constructor that is compatible with the toString() method is possible as well (optionally @JsonValue on it), although that is only viable for simple classes. That way a local solution can be chosen instead of adding several more classes for (de)serialising.

Just override toString() and the constructor。For example:

public SysUser(String both) {
    SysUser user = JSONUtil.toBean(both, SysUser.class);
    BeanUtil.copyProperties(user, this); 
}

@Override 
public String toString() {
    return JSONUtil.toJsonStr(this); 
}
Related