简体   繁体   中英

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

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.

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.

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).

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

The technical post webpages of this site follow the CC BY-SA 4.0 protocol. If you need to reprint, please indicate the site URL or the original address.Any question please contact:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM