With default typing enabled, is there a way to override the type Jackson outputs?

1.8k Views Asked by At

I am serializing a class that includes an unmodifiable list with default typing enabled. The problem is that the type that Jackson uses is

java.util.Collections$UnmodifiableRandomAccessList

which, for some reason, the deserializer does not know how to handle.

Is there a way to tell Jackson to set the type as

java.util.ArrayList

which the deserializer does know how to handle, instead? If possible, I'd like to do it using mixins.

Something like

public abstract class ObjectMixin {
    @JsonCreator
    public ObjectMixin(
       @JsonProperty("id") String id,
       @JsonProperty("list") @JsonSerialize(as = ArrayList.class) List<String> list;
    ) {}
}

which, unfortunately, does not work.

1

There are 1 best solutions below

1
On BEST ANSWER

I would like to start from security risk warning which comes from ObjectMapper documentation:

Notes on security: use "default typing" feature (see enableDefaultTyping()) is a potential security risk, if used with untrusted content (content generated by untrusted external parties). If so, you may want to construct a custom TypeResolverBuilder implementation to limit possible types to instantiate, (using setDefaultTyping(com.fasterxml.jackson.databind.jsontype.TypeResolverBuilder<?)).

Lets implement custom resolver:

class CollectionsDefaultTypeResolverBuilder extends ObjectMapper.DefaultTypeResolverBuilder {

    private final Map<String, String> notValid2ValidIds = new HashMap<>();

    public CollectionsDefaultTypeResolverBuilder() {
        super(ObjectMapper.DefaultTyping.OBJECT_AND_NON_CONCRETE);
        this._idType = JsonTypeInfo.Id.CLASS;
        this._includeAs = JsonTypeInfo.As.PROPERTY;

        notValid2ValidIds.put("java.util.Collections$UnmodifiableRandomAccessList", ArrayList.class.getName());
        // add more here...
    }

    @Override
    protected TypeIdResolver idResolver(MapperConfig<?> config, JavaType baseType, Collection<NamedType> subtypes,
                                        boolean forSer, boolean forDeser) {
        return new ClassNameIdResolver(baseType, config.getTypeFactory()) {
            @Override
            protected String _idFrom(Object value, Class<?> cls, TypeFactory typeFactory) {
                String id = notValid2ValidIds.get(cls.getName());
                if (id != null) {
                    return id;
                }
                return super._idFrom(value, cls, typeFactory);
            }
        };
    }
}

Now, we can use it as below:

import com.fasterxml.jackson.annotation.JsonTypeInfo;
import com.fasterxml.jackson.databind.JavaType;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.fasterxml.jackson.databind.SerializationFeature;
import com.fasterxml.jackson.databind.cfg.MapperConfig;
import com.fasterxml.jackson.databind.jsontype.NamedType;
import com.fasterxml.jackson.databind.jsontype.TypeIdResolver;
import com.fasterxml.jackson.databind.jsontype.impl.ClassNameIdResolver;
import com.fasterxml.jackson.databind.type.TypeFactory;

import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collection;
import java.util.Collections;
import java.util.HashMap;
import java.util.List;
import java.util.Map;

public class JsonApp {

    public static void main(String[] args) throws Exception {
        ObjectMapper mapper = new ObjectMapper();
        mapper.enable(SerializationFeature.INDENT_OUTPUT);
        mapper.setDefaultTyping(new CollectionsDefaultTypeResolverBuilder());

        Root root = new Root();
        root.setData(Collections.unmodifiableList(Arrays.asList("1", "b")));
        String json = mapper.writeValueAsString(root);
        System.out.println(json);
        System.out.println(mapper.readValue(json, Root.class));
    }
}

class Root {
    private List<String> data;

    public List<String> getData() {
        return data;
    }

    public void setData(List<String> data) {
        this.data = data;
    }

    @Override
    public String toString() {
        return "Root{" +
                "data=" + data +
                '}';
    }
}

Above code prints:

{
  "data" : [ "java.util.ArrayList", [ "1", "b" ] ]
}
Root{data=[1, b]}

You can even map it to List interface:

notValid2ValidIds.put("java.util.Collections$UnmodifiableRandomAccessList", List.class.getName());

And output would be:

{
  "data" : [ "java.util.List", [ "1", "b" ] ]
}