Prevent Jackson ObjectMapper automatically toString()-formatting composite map keys

Viewed 594

I'm using com.fasterxml.jackson.databind.ObjectMapper.writeValueAsString() to generate JSON from Java POJOs.

One of my objects - lets call it ResultSheet - contains a property Map<ResultId,Integer> results. ResultId represents a composite PK (of two FKs).

It appears that ObjectMapper upon encountering ResultId realizes that this can't be represented in JSON directly because JSON map keys have to be strings. It is therefore outputing the toString() of ResultId instead. (In this particular case this produces a string in the form "42:43" but only because this happens to be how toString() is currently implemented.)

How can I stop it doing this? The current behavour is hiding the fact that the structure being marshalled is not properly representable as a JSON structure. It is also leaking the current toString() implementation into the API of my web service.

I would rather have an exception thrown telling me when a value used as a map key is not a String - or, at least, when it is not trivial to convert to a String. (I'm not so concerned about Long keys being toString()-ed, for example.)

I've looked into the SerializationFeature settings but none of them seem to address this issue. https://fasterxml.github.io/jackson-databind/javadoc/2.7/com/fasterxml/jackson/databind/SerializationFeature.html

Is there are way to prevent this? Can it be done in a general way i.e. not just for this specific class but in any case where this might arise on future, as new classes, some of which might be used as map keys, are added?

Note, making toString() throw an exception would not be an acceptable hack in this instance.

1 Answers

To solve this problem you can implement and register your own com.fasterxml.jackson.databind.ser.BeanSerializerModifier class. And override modifyKeySerializer method. Implementation could always return a serialiser which throws an exception. See below example:

import com.fasterxml.jackson.core.JsonGenerator;
import com.fasterxml.jackson.databind.BeanDescription;
import com.fasterxml.jackson.databind.JavaType;
import com.fasterxml.jackson.databind.JsonSerializer;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.fasterxml.jackson.databind.SerializationConfig;
import com.fasterxml.jackson.databind.SerializerProvider;
import com.fasterxml.jackson.databind.module.SimpleModule;
import com.fasterxml.jackson.databind.ser.BeanSerializerModifier;
import lombok.AllArgsConstructor;
import lombok.Data;

import java.io.IOException;
import java.util.Collections;
import java.util.Map;

public class JsonKeySerializerApp {
    public static void main(String[] args) throws IOException {
        SimpleModule keyExceptionSerializerModule = new SimpleModule();
        keyExceptionSerializerModule.setSerializerModifier(new BeanSerializerModifier() {
            @Override
            public JsonSerializer<?> modifyKeySerializer(SerializationConfig config, JavaType valueType, BeanDescription beanDesc, JsonSerializer<?> serializer) {
                // specify classes you want to allow to be serialized as keys
                if (valueType.getRawClass().getPackage().getName().startsWith("java.")) {
                    return serializer;
                }
                return new JsonSerializer<Object>() {
                    @Override
                    public void serialize(Object value, JsonGenerator gen, SerializerProvider serializers) {
                        throw new UnsupportedOperationException("You can not serialize POJO as keys!");
                    }
                };
            }
        });
        ObjectMapper mapper = new ObjectMapper();
        mapper.registerModule(keyExceptionSerializerModule);

        Map<ResultId, Long> map = Collections.singletonMap(new ResultId(1, 2), 1L);

        mapper.writeValue(System.out, map);
    }
}

@AllArgsConstructor
@Data
class ResultId {

    private int id1;
    private int id2;
}

Above code prints:

{Exception in thread "main" com.fasterxml.jackson.databind.JsonMappingException: You can not serialize POJO as keys! (through reference chain: java.util.Collections$SingletonMap["ResultId(id1=1, id2=2)"])
    at com.fasterxml.jackson.databind.JsonMappingException.wrapWithPath(JsonMappingException.java:394)
    at com.fasterxml.jackson.databind.JsonMappingException.wrapWithPath(JsonMappingException.java:353)
    at com.fasterxml.jackson.databind.ser.std.StdSerializer.wrapAndThrow(StdSerializer.java:316)
    at com.fasterxml.jackson.databind.ser.std.MapSerializer.serializeFields(MapSerializer.java:725)
    at com.fasterxml.jackson.databind.ser.std.MapSerializer.serialize(MapSerializer.java:643)
    at com.fasterxml.jackson.databind.ser.std.MapSerializer.serialize(MapSerializer.java:33)
Related