Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Jackson DeSerializer for Class<? extends Foo>

I want to have a Serializer and Deserializer for Class Literals. It should be mapped from / to JSON Strings. And it should also work with generic classes.

I tried writing a custom Deserializer, but it handles all Classes, not just the ones I want:

public class MyDeserializer extends JsonDeserializer<Class<? extends Foo>> {
    public Class<? extends Foo> deserialize(JsonParser jp, DeserializationContext ctxt) 
        throws IOException, JsonProcessingException {

        String token = jp.getText();
        switch(token) {
            case "FOO": return Foo.class;
            case "BAR": return Bar.class;
        }
        return null;
    }
}

Jackson will call this Deserializer for EVERY Class<?>. But I just want it to get called for Class<? extends Foo>.

Btw: I want to register this as a global Deserializer. I don't want to use @JsonDeserialize everywhere.

How can I get this working properly?

Thanks in advance.

EDIT: Some more code

POJO:

public class MyPOJO {
    public Class<? extends Foo> fooType;
}

JSON:

{
    "fooType": "FOO"
}

ObjectMapper usage:

MyPOJO pojo = new MyPOJO();
pojo.fooType = Foo.class;

ObjectMapper om = new ObjectMapper();
String json = om.writeValueAsString(pojo);
like image 418
Benjamin M Avatar asked Sep 10 '25 17:09

Benjamin M


1 Answers

What you try to achieve seems to be impossible due to Type Erasure, indeed at runtime your class MyDeserializer is seen as a JsonDeserializer<Class> and your field fooType is simply of type Class not of type Class<? extends Foo> that is actually why your deserializer is called for every Class<?>.

In your case the best solution would be to annotate your fields of type Class<? extends Foo> with @JsonDeserialize(using = MyDeserializer.class) in order to indicate at runtime the deserializer to use but as you don't want to do it, as workaround, assuming that in given class when you have fields of type Class they will all use the same deserializer (custom or default), you could simply modify your class MyDeserializer to check first the current bean class to know whether we should use the custom deserializer or the default one.

You would register it for example as next:

ObjectMapper mapper = new ObjectMapper();
SimpleModule module = new SimpleModule("MyModule")
    .addDeserializer(Class.class, new MyDeserializer());
mapper.registerModule(module);

The code of your deserializer would then be:

public class MyDeserializer extends JsonDeserializer<Class<?>>  {

    // The default deserializer for a property of type Class
    private final FromStringDeserializer.Std defaultDeserializer;

    public MyDeserializer() {
        // Set the default deserializer
        this.defaultDeserializer = FromStringDeserializer.findDeserializer(Class.class);
    }

    public Class<?> deserialize(JsonParser jp, DeserializationContext ctxt) 
        throws IOException, JsonProcessingException {
        // Check whether we should used the custom or default deserializer
        // based on the bean class
        if (accept(jp.getCurrentValue().getClass())) {
            // The custom deserializer
            String token = jp.getText();
            switch(token) {
                case "FOO": return Foo.class;
                case "BAR": return Bar.class;
            }
            return null;
        }
        // Call the default deserializer
        return (Class<?>) defaultDeserializer.deserialize(jp, ctxt);
    }

    // Returns true if this bean class has fields of type Class that must
    // be deserialized with the custom deserializer, false otherwise
    public boolean accept(Class<?> beanClass) {
        // Implement your logic here
        // You could for example have all the bean classes in a Set and 
        // check that the provided class is in the Set 
    }
}
like image 75
Nicolas Filotto Avatar answered Sep 13 '25 05:09

Nicolas Filotto