26

Now i'm working with Jackson and i have some questions about it.

First of all. I have two services, first is data collecting and sending service and second receive this data and, for example, log it into a file.

So, first service has class hierarchy like this:

         +----ConcreteC
         |
Base ----+----ConcreteA
         |
         +----ConcreteB

And second service has class hierarchy like this:

ConcreteAAdapter extends ConcreteA implements Adapter {}
ConcreteBAdapter extends ConcreteB implements Adapter {}
ConcreteCAdapter extends ConcreteC implements Adapter {}

The first service knows nothing about ConcreteXAdapter.

The way i'm sending the data on the first service:

Collection<Base> data = new LinkedBlockingQueue<Base>()
JacksonUtils utils = new JacksonUtils();
data.add(new ConcreteA());
data.add(new ConcreteB());
data.add(new ConcreteC());
...
send(utils.marshall(data));
...

public class JacksonUtils {

    public byte[] marshall(Collection<Base> data) throws IOException {
        ByteArrayOutputStream out = new ByteArrayOutputStream() {
            @Override
            public byte[] toByteArray() {
                return buf;
            }
        };

        getObjectMapper().writeValue(out, data);
        return out.toByteArray();
    }
    protected ObjectMapper getObjectMapper() {
        return new ObjectMapper();
    }

    public Object unmarshall(byte[] json) throws IOException {
        return getObjectMapper().readValue(json, Object.class);
    }

    public <T> T unmarshall(InputStream source, TypeReference<T> typeReference) throws IOException {
        return getObjectMapper().readValue(source, typeReference);
    }

    public <T> T unmarshall(byte[] json, TypeReference<T> typeReference) throws IOException {
        return getObjectMapper().readValue(json, typeReference);
    }
}

So, i want to desirialize json into Collection of ConcreteXAdapter, not into Collection of ConcreteX (ConcreteA -> ConcreteAAdapter, ConcreteB -> ConcreteBAdapter, ConcreteC -> ConcreteCAdapter). In the case i described i want to get:

Collection [ConcreteAAdapter, ConcreteBAdapter, ConcreteCAdapter]

How can i do this?

pbespechnyi
  • 2,251
  • 1
  • 19
  • 29

3 Answers3

32

For this purpose you need to pass additional info in JSON:

@JsonTypeInfo(use=JsonTypeInfo.Id.NAME, 
      include=JsonTypeInfo.As.PROPERTY, property="@type")
class Base {
...
}

Then on serialization it will add @type field:

objectMapper.registerSubtypes(
            new NamedType(ConcreteAAdapter.class, "ConcreteA"),
            new NamedType(ConcreteBAdapter.class, "ConcreteB"),
            new NamedType(ConcreteCAdapter.class, "ConcreteC")
            );

// note, that for lists you need to pass TypeReference explicitly
objectMapper.writerWithType(new TypeReference<List<Base>>() {})
     .writeValueAsString(someList);


    {
      "@type" : "ConcreteA",
      ...
    }

on deserialization it will be:

    objectMapper.registerSubtypes(
            new NamedType(ConcreteA.class, "ConcreteA"),
            new NamedType(ConcreteB.class, "ConcreteB"),
            new NamedType(ConcreteC.class, "ConcreteC")
            );
    objectMapper.readValue(....)

More info here

Luke
  • 186
  • 1
  • 7
Eugene Retunsky
  • 13,009
  • 4
  • 52
  • 55
  • Thanks for the fast reply! I updated the question. Briefly, first service knows nothing about `ConcreteXAdapter`. So the question is how to tell Jackson to create `ConcreteXAdapter` if it found `ConcreteX` in json. – pbespechnyi Apr 26 '12 at 20:20
  • 1
    In this case I would suggest to pass type manually (like "@type" : "ConcreteA") and then deserialize on the other side based on the property. I.e. you need to implement a custom serializer for this. – Eugene Retunsky Apr 26 '12 at 20:29
  • So there is no way to do this out of the box. Ok, thanks for the help! – pbespechnyi Apr 26 '12 at 20:37
  • I would serialize ConcreteA/B/C classes in this case, not adapters. If you need to extend them - then you can use composition instead (which is, in most cases, better than inheritance). – Eugene Retunsky Apr 26 '12 at 20:42
  • Yep, i know that, but i can't change Adapters in such way, because it will affect other systems. – pbespechnyi Apr 26 '12 at 20:57
  • 2
    @Eugene there is support: just use `@JsonTypeName`, and 'As.NAME' -- NOT the class name, if those differ. You do then need to register subtypes (either by calling `ObjectMapper.registerSubtypes()`, or by using @JsonSubTypes annotation). – StaxMan Apr 26 '12 at 21:47
20

How I solved this problem. Here is a class diagram for an example project: class diagram

So i want to get the ConcreteAAdapter form ConcreteA after deserialization.

My solution is to extend ClassNameIdResolver to add functionality to deserialize base class objects into subtype class objects (subtype classes adds no extra functionality and additional fields).

Here is a code which creates ObjectMapper for deserialization:

protected ObjectMapper getObjectMapperForDeserialization() {
        ObjectMapper mapper = new ObjectMapper();

        StdTypeResolverBuilder typeResolverBuilder = new ObjectMapper.DefaultTypeResolverBuilder(ObjectMapper.DefaultTyping.OBJECT_AND_NON_CONCRETE);
        typeResolverBuilder = typeResolverBuilder.inclusion(JsonTypeInfo.As.PROPERTY);
        typeResolverBuilder.init(JsonTypeInfo.Id.CLASS, new ClassNameIdResolver(SimpleType.construct(Base.class), TypeFactory.defaultInstance()) {
            private HashMap<Class, Class> classes = new HashMap<Class, Class>() {
                {
                    put(ConcreteA.class, ConcreteAAdapter.class);
                    put(ConcreteB.class, ConcreteBAdapter.class);
                    put(ConcreteC.class, ConcreteCAdapter.class);
                }
            };

            @Override
            public String idFromValue(Object value) {
                return (classes.containsKey(value.getClass())) ? value.getClass().getName() : null;
            }

            @Override
            public JavaType typeFromId(String id) {
                try {
                    return classes.get(Class.forName(id)) == null ? super.typeFromId(id) : _typeFactory.constructSpecializedType(_baseType, classes.get(Class.forName(id)));
                } catch (ClassNotFoundException e) {
                    // todo catch the e
                }
                return super.typeFromId(id);
            }
        });
        mapper.setDefaultTyping(typeResolverBuilder);
        return mapper;
    }

And here is a code which create ObjectMapper for serialization:

protected ObjectMapper getObjectMapperForSerialization() {
    ObjectMapper mapper = new ObjectMapper();

    StdTypeResolverBuilder typeResolverBuilder = new ObjectMapper.DefaultTypeResolverBuilder(ObjectMapper.DefaultTyping.OBJECT_AND_NON_CONCRETE);
    typeResolverBuilder = typeResolverBuilder.inclusion(JsonTypeInfo.As.PROPERTY);
    typeResolverBuilder.init(JsonTypeInfo.Id.CLASS, new ClassNameIdResolver(SimpleType.construct(Base.class), TypeFactory.defaultInstance()));
    mapper.setDefaultTyping(typeResolverBuilder);

    return mapper;
}

Test code:

public static void main(String[] args) throws IOException {
    JacksonUtils JacksonUtils = new JacksonUtilsImpl();

    Collection<Base> data = new LinkedBlockingQueue<Base>();
    data.add(new ConcreteA());
    data.add(new ConcreteB());
    data.add(new ConcreteC());

    String json = JacksonUtils.marshallIntoString(data);

    System.out.println(json);

    Collection<? extends Adapter> adapters = JacksonUtils.unmarshall(json, new TypeReference<ArrayList<Adapter>>() {});

    for (Adapter adapter : adapters) {
        System.out.println(adapter.getClass().getName());
    }
}

Full code of JacksonUtils class:

public class JacksonUtilsImpl implements JacksonUtils {

    @Override
    public byte[] marshall(Collection<Base> data) throws IOException {
        ByteArrayOutputStream out = new ByteArrayOutputStream() {
            @Override
            public byte[] toByteArray() {
                return buf;
            }
        };

        getObjectMapperForSerialization().writerWithType(new TypeReference<Collection<Base>>() {}).writeValue(out, data);
        return out.toByteArray();
    }

    @Override
    public String marshallIntoString(Collection<Base> data) throws IOException {
        return getObjectMapperForSerialization().writeValueAsString(data);
    }

    protected ObjectMapper getObjectMapperForSerialization() {
        ObjectMapper mapper = new ObjectMapper();

        StdTypeResolverBuilder typeResolverBuilder = new ObjectMapper.DefaultTypeResolverBuilder(ObjectMapper.DefaultTyping.OBJECT_AND_NON_CONCRETE);
        typeResolverBuilder = typeResolverBuilder.inclusion(JsonTypeInfo.As.PROPERTY);
        typeResolverBuilder.init(JsonTypeInfo.Id.CLASS, new ClassNameIdResolver(SimpleType.construct(Base.class), TypeFactory.defaultInstance()));
        mapper.setDefaultTyping(typeResolverBuilder);

        return mapper;
    }

    protected ObjectMapper getObjectMapperForDeserialization() {
        ObjectMapper mapper = new ObjectMapper();

        StdTypeResolverBuilder typeResolverBuilder = new ObjectMapper.DefaultTypeResolverBuilder(ObjectMapper.DefaultTyping.OBJECT_AND_NON_CONCRETE);
        typeResolverBuilder = typeResolverBuilder.inclusion(JsonTypeInfo.As.PROPERTY);
        typeResolverBuilder.init(JsonTypeInfo.Id.CLASS, new ClassNameIdResolver(SimpleType.construct(Base.class), TypeFactory.defaultInstance()) {
            private HashMap<Class, Class> classes = new HashMap<Class, Class>() {
                {
                    put(ConcreteA.class, ConcreteAAdapter.class);
                    put(ConcreteB.class, ConcreteBAdapter.class);
                    put(ConcreteC.class, ConcreteCAdapter.class);
                }
            };

            @Override
            public String idFromValue(Object value) {
                return (classes.containsKey(value.getClass())) ? value.getClass().getName() : null;
            }

            @Override
            public JavaType typeFromId(String id) {
                try {
                    return classes.get(Class.forName(id)) == null ? super.typeFromId(id) : _typeFactory.constructSpecializedType(_baseType, classes.get(Class.forName(id)));
                } catch (ClassNotFoundException e) {
                    // todo catch the e
                }
                return super.typeFromId(id);
            }
        });
        mapper.setDefaultTyping(typeResolverBuilder);
        return mapper;
    }

    @Override
    public Object unmarshall(byte[] json) throws IOException {
        return getObjectMapperForDeserialization().readValue(json, Object.class);
    }

    @Override
    public <T> T unmarshall(InputStream source, TypeReference<T> typeReference) throws IOException {
        return getObjectMapperForDeserialization().readValue(source, typeReference);
    }

    @Override
    public <T> T unmarshall(byte[] json, TypeReference<T> typeReference) throws IOException {
        return getObjectMapperForDeserialization().readValue(json, typeReference);
    }

    @Override
    public <T> Collection<? extends T> unmarshall(String json, Class<? extends Collection<? extends T>> klass) throws IOException {
        return getObjectMapperForDeserialization().readValue(json, klass);
    }


    @Override
    public <T> Collection<? extends T> unmarshall(String json, TypeReference typeReference) throws IOException {
        return getObjectMapperForDeserialization().readValue(json, typeReference);
    }
}
pbespechnyi
  • 2,251
  • 1
  • 19
  • 29
  • This is a more clean solution, sometimes it is not possible to add extra information to the json messages, for example for existing REST APIs that customers are already using or when classes are been auto generated, e.g. open-api – raspacorp Sep 12 '20 at 06:00
13

I find programmerbruce's approach to be the most clear and easy to get working (example below). I got the information from his answer to a related question: https://stackoverflow.com/a/6339600/1148030 and the related blog post: http://programmerbruce.blogspot.fi/2011/05/deserialize-json-with-jackson-into.html

Also check out this friendly wiki page (also mentioned in Eugene Retunsky's answer): https://github.com/FasterXML/jackson-docs/wiki/JacksonPolymorphicDeserialization

Another nice wiki page: https://github.com/FasterXML/jackson-docs/wiki/JacksonMixInAnnotations

Here is a short example to give you the idea:

Configure the ObjectMapper like this:

    mapper.getDeserializationConfig().addMixInAnnotations(Base.class, BaseMixin.class);
    mapper.getSerializationConfig().addMixInAnnotations(Base.class, BaseMixin.class);

Example BaseMixin class (easy to define as an inner class.)

@JsonTypeInfo(use=JsonTypeInfo.Id.NAME, include=JsonTypeInfo.As.PROPERTY, property="type")
@JsonSubTypes({
    @JsonSubTypes.Type(value=ConcreteA.class, name="ConcreteA"),
    @JsonSubTypes.Type(value=ConcreteB.class, name="ConcreteB")
})  
private static class BaseMixin {
}

On second service you could define the BaseMixin like this:

@JsonTypeInfo(use=JsonTypeInfo.Id.NAME, include=JsonTypeInfo.As.PROPERTY, property="type")
@JsonSubTypes({
    @JsonSubTypes.Type(value=ConcreteAAdapter.class, name="ConcreteA"),
    @JsonSubTypes.Type(value=ConcreteBAdapter.class, name="ConcreteB")
})  
private static class BaseMixin {
}
Peter Lamberg
  • 8,151
  • 3
  • 55
  • 69
  • 1
    The links to fasterxml.com are no longer working. The new links are https://github.com/FasterXML/jackson-docs/wiki/JacksonMixInAnnotations and https://github.com/FasterXML/jackson-docs/wiki/JacksonPolymorphicDeserialization – Wim Deblauwe Mar 19 '19 at 07:06
  • For this question, Mixin is really a nice idea for decoupling jackson configurations from business codes. – Zouyiu Ng Apr 29 '21 at 06:40