70

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.

Raedwald
  • 46,613
  • 43
  • 151
  • 237
Mick Knutson
  • 2,297
  • 3
  • 25
  • 48
  • Regarding 2.9 and potentially other versions: I understand the issue https://github.com/FasterXML/jackson-databind/issues/1419 like `Map` serialization with annotated properties simply doesn't work and Jackson fails with an unrelated error message. – Kalle Richter Mar 11 '18 at 12:54

5 Answers5

50

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.

estebanrv
  • 693
  • 5
  • 9
  • 1
    Do you know how to register the module with Resteasy's context loaded through Spring? I don't know how to access the automatically built objectMapper – Fractaliste Jun 20 '18 at 19:57
  • @Fractaliste that's for another question. But in short: You override it with your own bean. `@Bean fun myMapper(): ObjectMapper { ... }` – Ondra Žižka Jan 28 '21 at 02:58
  • What to do if the key is another entity stored as JSON? Can I safely use the mapper being created inside of my key deserializer? – Ondra Žižka Jan 28 '21 at 02:59
31

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

Dennis Meng
  • 5,109
  • 14
  • 33
  • 36
StaxMan
  • 113,358
  • 34
  • 211
  • 239
  • 2
    After a quick test, I think keys are serialized using their toString method, which is probably often not what you want. Creating a key serializer seems like a good option. – Suma Jul 14 '15 at 12:12
  • 4
    That is the fallback if nothing else is found. There are alternatives: if the type has a public single-String-argument constructor, for example, that will be used. Or a single-String-argument factory method that is annotated with `@JsonCreator`. This will remove the need to write and register a custom key deserializer. – StaxMan Jul 15 '15 at 00:02
  • 1
    Great - but you describe deserializer (I will try that). What about serializer? In my case toString was used (for a Scala case class). What other options are there for serializer, besides of writing my own? – Suma Jul 15 '15 at 07:04
  • 2
    Right, for serializer, use of `@JsonValue` should work, although I have not tested that; there are some gaps in support for annotations with key (de)serializers. Aside from this, custom key serializer is an option, either globally register or using `@JsonSerialize(keyUsing=MyKeySerializer.class` for class or property. – StaxMan Jul 17 '15 at 19:25
1

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

    }

}
gagarwa
  • 1,426
  • 1
  • 15
  • 28
0

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.

Redshift
  • 11
  • 1
  • 2
0

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); 
}
Matrix
  • 1
  • 2