type handling of custom serializer and deserializer

2,064 views
Skip to first unread message

nathan leung

unread,
Feb 24, 2014, 11:27:33 AM2/24/14
to jackso...@googlegroups.com
Hi,

I have a question regarding the following scenario:

Class C implements Interface I
Register a custom serializer and deserializer for Interface I
Serialize using: String json = ObjectMapper mapper.writerWithType(C.class).writeValueAsString(instance of C); // writes json using custom serializer
Deserialize using mapper.readValue(json, I.class);  // this works, uses custom deserializer
Deserialize using: mapper.readValue(json, C.class); // this throws an exception

Why is it that when I serialize using the writer for C.class, it uses the custom serialization, but when I deserialize using C.class it does not use the custom deserialization?  Code example and program output follow:

import com.fasterxml.jackson.core.*;
import com.fasterxml.jackson.databind.*;
import com.fasterxml.jackson.databind.module.SimpleModule;
import org.junit.Assert;

import java.io.IOException;

public class JacksonTest {

    public static interface IntegerInterface {
        Integer getValue();
        void setValue(Integer val);
    }

    public static class Number implements IntegerInterface {
        private Integer value;

        public Number() {
            value = 0;
        }
        public Number(int v) {
            value = v;
        }
        @Override
        public String toString() {
            return value.toString();
        }
        @Override
        public Integer getValue() { return value; }
        @Override
        public void setValue(Integer val) { value = val; }
    }

    public static class IntegerInterfaceSerializer extends com.fasterxml.jackson.databind.JsonSerializer<IntegerInterface> {

        @Override
        public void serialize(IntegerInterface num, JsonGenerator jsonGenerator, SerializerProvider serializerProvider) throws IOException {
            jsonGenerator.writeStartObject();
            jsonGenerator.writeObjectField("type", num.getClass().getName());
            jsonGenerator.writeObjectField("my value is", num.getValue());
            jsonGenerator.writeEndObject();
        }

        @Override
        public Class<IntegerInterface> handledType() {
            return IntegerInterface.class;
        }
    }

    public static class IntegerInterfaceDeserializer extends JsonDeserializer<IntegerInterface> {
        @Override
        public IntegerInterface deserialize(JsonParser jsonParser, DeserializationContext deserializationContext) throws IOException {
            ObjectMapper mapper = (ObjectMapper)jsonParser.getCodec();
            TreeNode node = mapper.readTree(jsonParser);
            String typeString = ((JsonNode)node.get("type")).asText();
            IntegerInterface newInstance;
            try {
                newInstance = (IntegerInterface)Class.forName(typeString).newInstance();
            } catch (Exception e) {
                return null;
            }
            newInstance.setValue(((JsonNode)node.get("my value is")).asInt());
            return newInstance;
        }
    }

    public static void main(String args[]) throws Exception {
        ObjectMapper mapper = new ObjectMapper();

        SimpleModule module = new SimpleModule();
        module.addSerializer(IntegerInterface.class, new IntegerInterfaceSerializer());
        module.addDeserializer(IntegerInterface.class, new IntegerInterfaceDeserializer());
        mapper.registerModule(module);

        Number number = new Number(1234);

        String numberString = mapper.writerWithType(Number.class).writeValueAsString(number);
        System.out.println("Number string: " + numberString);
        IntegerInterface deserNumber0 = mapper.readValue(numberString, IntegerInterface.class);
        System.out.println("Deserialized Number (IntegerInterface.class): " + deserNumber0);
        IntegerInterface deserNumber1 = mapper.readValue(numberString, Number.class);
        System.out.println("Deserialized Number (Number.class): " + deserNumber1);
    }
}

Program Output:

Number string: {"type":"JacksonTest$Number","my value is":1234}
Deserialized Number (IntegerInterface.class): 1234
Exception in thread "main" com.fasterxml.jackson.databind.exc.UnrecognizedPropertyException: Unrecognized field "type" (class JacksonTest$Number), not marked as ignorable (one known property: "value"])
 at [Source: java.io.StringReader@67cc3210; line: 1, column: 10] (through reference chain: Number["type"])
at com.fasterxml.jackson.databind.exc.UnrecognizedPropertyException.from(UnrecognizedPropertyException.java:79)
at com.fasterxml.jackson.databind.DeserializationContext.reportUnknownProperty(DeserializationContext.java:555)
at com.fasterxml.jackson.databind.deser.std.StdDeserializer.handleUnknownProperty(StdDeserializer.java:708)
at com.fasterxml.jackson.databind.deser.BeanDeserializerBase.handleUnknownProperty(BeanDeserializerBase.java:1160)
at com.fasterxml.jackson.databind.deser.BeanDeserializer.deserializeFromObject(BeanDeserializer.java:315)
at com.fasterxml.jackson.databind.deser.BeanDeserializer.deserialize(BeanDeserializer.java:121)
at com.fasterxml.jackson.databind.ObjectMapper._readMapAndClose(ObjectMapper.java:2888)
at com.fasterxml.jackson.databind.ObjectMapper.readValue(ObjectMapper.java:2034)
at JacksonTest.main(JacksonTest.java:83)

Thanks,
Nathan

Tatu Saloranta

unread,
Feb 24, 2014, 12:42:38 PM2/24/14
to jackso...@googlegroups.com
Not sure. Seems like custom deserializer is not being used, even though it looks like it should be registered.

One simplification you could use for deserialization would be to use converting deserializer; either by using annotation as per:

http://www.cowtowncoder.com/blog/archives/2013/08/entry_480.html

or by extending StdDelegatingDeserializer. Benefit is just that conversion to a tree is done automatically and you only need to add extraction.

-+ Tatu +-



--
You received this message because you are subscribed to the Google Groups "jackson-user" group.
To unsubscribe from this group and stop receiving emails from it, send an email to jackson-user...@googlegroups.com.
To post to this group, send email to jackso...@googlegroups.com.
For more options, visit https://groups.google.com/groups/opt_out.

nathan leung

unread,
Feb 25, 2014, 10:18:09 AM2/25/14
to jackso...@googlegroups.com
Hi Tatu,

Thank you for your quick reply.  In the above example, after the IntegerInterface is registered with the serializer, is it possible to use the existing mapper to do a Bean serialization of an instance of Number?

Thanks,
Nathan

Tatu Saloranta

unread,
Feb 25, 2014, 1:31:30 PM2/25/14
to jackso...@googlegroups.com
On Tue, Feb 25, 2014 at 7:18 AM, nathan leung <ncl...@gmail.com> wrote:
Hi Tatu,

Thank you for your quick reply.  In the above example, after the IntegerInterface is registered with the serializer, is it possible to use the existing mapper to do a Bean serialization of an instance of Number?


Yes, mapper (and ObjectWriter) instance should use default serializer for Number, unless overridden by a custom serializer.

Since this looks like a potential bug, could you file an issue for jackson-databind, with the code sample from the original email? I can have a look, and this is the best way to try to avoid issue being forgotten about (I have a longish pipeline of things to work on, but one of us will get there eventually).

-+ Tatu +-

Tatu Saloranta

unread,
Feb 26, 2014, 2:13:31 AM2/26/14
to jackso...@googlegroups.com
Fixed the reported bug (#411).

-+ Tatu +-

nathan leung

unread,
Feb 26, 2014, 12:00:07 PM2/26/14
to jackso...@googlegroups.com
Hi Tatu,

I believe this is a different bug.  I've filed a new bug here: https://github.com/FasterXML/jackson-databind/issues/416

Thanks,
Nathan

Tatu Saloranta

unread,
Mar 1, 2014, 6:48:46 PM3/1/14
to jackso...@googlegroups.com
As per updates to that bug, the specific issue here is that registration of deserializers has to be more complete -- only exact type matches work with `SimpleModule` (whereas serializers can be registered using only base types).
So either you need to add more registration calls, or implement more advanced matching from types to deserializer for your module (sub-class `SimpleModule`, override appropriate deserializer lookup method; or implement `Module` directly).

I hope this helps,

-+ Tatu +-

Nathan Leung

unread,
Mar 1, 2014, 7:07:41 PM3/1/14
to jackso...@googlegroups.com
Thanks Tatu.  Is it possible to register a serializer to just use a BeanSerializer for the class?


--
You received this message because you are subscribed to a topic in the Google Groups "jackson-user" group.
To unsubscribe from this topic, visit https://groups.google.com/d/topic/jackson-user/8KBFqcgyWkw/unsubscribe.
To unsubscribe from this group and all its topics, send an email to jackson-user...@googlegroups.com.

Tatu Saloranta

unread,
Mar 1, 2014, 7:58:26 PM3/1/14
to jackso...@googlegroups.com
Yes, as long

Tatu Saloranta

unread,
Mar 1, 2014, 7:59:28 PM3/1/14
to jackso...@googlegroups.com
Yes, as long as `handledType()` of serializer implementation returns correct super-type used.
This is the case for standard serializers, and custom serializers that use standard base classes.

-+ Tatu +-

nathan leung

unread,
Mar 6, 2014, 5:06:39 PM3/6/14
to jackso...@googlegroups.com
Hi Tatu,

Sorry to dig this one back up.  I noticed it's possible to create a BeanSerializer for Number.class if I haven't registered any serializers for IntegerInterface.class.  Please consider the following code/output (based on the classes/interfaces I posted before):

        ObjectMapper mapper = new ObjectMapper();
        Number number = new Number(1234);

        // note here that I'm getting a writer before registering the custom serializer
        // if I comment this line of code then both writeValueAsString calls below use the custom
        // serializer registered for IntegerInterface.class
        mapper.writerWithType(Number.class);

        SimpleModule module = new SimpleModule();
        module.addSerializer(IntegerInterface.class, new IntegerInterfaceSerializer());
        module.addDeserializer(IntegerInterface.class, new IntegerInterfaceDeserializer());
        mapper.registerModule(module);

        String numberString = mapper.writerWithType(Number.class).writeValueAsString(number);
        System.out.println("Number string: " + numberString);

        String integerInterfaceString = mapper.writerWithType(IntegerInterface.class).writeValueAsString(number);
        System.out.println("IntegerInterface string: " + integerInterfaceString);

Yields:

        Number string: {"value":1234}
        IntegerInterface string: {"type":"com.realmedia.turbine.common.serialization.JacksonTest$Number","my value is":1234}

This is the behavior I would like.  However, I may not always know all classes implementing IntegerInterface when I'm registering my module, so I cannot always create the BeanSerializers for the implementing classes ahead of time.  Is there an easy way to create and register the BeanSerializer for Number.class after I've registered my module?  If so, can you point me to an example?

Thanks,
Nathan

Tatu Saloranta

unread,
Mar 6, 2014, 6:38:19 PM3/6/14
to jackso...@googlegroups.com
On Thu, Mar 6, 2014 at 2:06 PM, nathan leung <ncl...@gmail.com> wrote:
Hi Tatu,

Sorry to dig this one back up.  I noticed it's possible to create a BeanSerializer for Number.class if I haven't registered any serializers for IntegerInterface.class.  Please consider the following code/output (based on the classes/interfaces I posted before):


First things first: all configuration with modules MUST be done before any use of ObjectMapper.
Whether any configuration done after first use takes effect is not guaranteed.
So code below may happen to work, but is not really specified to do so.
 
        ObjectMapper mapper = new ObjectMapper();
        Number number = new Number(1234);

        // note here that I'm getting a writer before registering the custom serializer
        // if I comment this line of code then both writeValueAsString calls below use the custom
        // serializer registered for IntegerInterface.class
        mapper.writerWithType(Number.class);

        SimpleModule module = new SimpleModule();
        module.addSerializer(IntegerInterface.class, new IntegerInterfaceSerializer());
        module.addDeserializer(IntegerInterface.class, new IntegerInterfaceDeserializer());
        mapper.registerModule(module);

        String numberString = mapper.writerWithType(Number.class).writeValueAsString(number);
        System.out.println("Number string: " + numberString);

        String integerInterfaceString = mapper.writerWithType(IntegerInterface.class).writeValueAsString(number);
        System.out.println("IntegerInterface string: " + integerInterfaceString);

Yields:

        Number string: {"value":1234}
        IntegerInterface string: {"type":"com.realmedia.turbine.common.serialization.JacksonTest$Number","my value is":1234}

This is the behavior I would like.  However, I may not always know all classes implementing IntegerInterface when I'm registering my module, so I cannot always create the BeanSerializers for the implementing classes ahead of time.  Is there an easy way to create and register the BeanSerializer for Number.class after I've registered my module?  If so, can you point me to an example?

In that case, do not use `SimpleModule`; you will need to use custom `Serializers` implementation.
SimpleModule uses `SimpleSerializers` which requires static mapping: but this is not required `Serializers` in general. You can implement your own resolution and construct serializer instances dynamically.
You may be able to override registration that SimpleModule uses, but code is pretty simple: all it needs to do is register `Serializers` instance on mapper.
Reply all
Reply to author
Forward
0 new messages