Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

JacksonPolymorphicDeserialization: JsonMappingException

I supposed that I had a parent Class Parameter that has 2 sub classes ComboParameter and IntegerParameter

@JsonSubTypes({
    @JsonSubTypes.Type(value = IntegerParameter.class, name = "integerParam"),
    @JsonSubTypes.Type(value = ComboParameter.class, name = "comboParam")
})
@JsonTypeInfo(use = JsonTypeInfo.Id.NAME, include = As.WRAPPER_OBJECT)
 public abstract class Parameter {
String regEx;
}


@JsonTypeName("integerParam")
public class IntegerParameter extends Parameter {
}

@JsonTypeName("comboParam")
public class ComboParameter extends Parameter {
List<String> values;
}

And I have a class that had an attribute parameter

class A {
@JsonUnwrapped
Parameter parameter;
}

The Serialization of an Object A throw an Exception

com.fasterxml.jackson.databind.JsonMappingException: Unwrapped property requires use of type information: can not serialize without disabling SerializationFeature.FAIL_ON_UNWRAPPED_TYPE_IDENTIFIERS

And if I delete the annotation @JsonUnwrapped I will have a json like That

{
     parameter:{
          integerParam:{
               regEx: regExVal
          }
     }
}

And what I need is a json like that:

{
     integerParam:{
           regEx: regExVal
     }
}

NB I' am using Jackson 2.4.4

like image 908
Forgotten Angel Avatar asked Mar 11 '15 17:03

Forgotten Angel


1 Answers

Don't think there is easy and clean solution for this problem. But here is some thoughts how you can solve it (Gist demo for both cases):

Option 1: Add @JsonIgnore above property and @JsonAnyGetter in the top level bean. Easy to implement but not nice to have static ObjectMapper in bean and will have to copy this code to every been with Parameter property

public class A {

    @JsonIgnore
    Parameter parameter;

    // can put ObjectMapper and most of this code in util class
    // and just use JacksonUtils.toMap(parameter) as return of JsonAnyGetter

    private static ObjectMapper mapper = new ObjectMapper();

    /************************ Serialization ************************/

    @JsonAnyGetter
    private Map<String, Object> parameterAsMap(){
        return mapper.convertValue(parameter, Map.class); 
    }

    /************************ Deserialization **********************/

    @JsonAnySetter
    private void parameterFromMap(String key, JsonNode value)  {
        try {
            parameter =  mapper.readValue(String.format("{\"%s\":%s}", key,value), 
                    Parameter.class);
        } catch (IOException e) {
            e.printStackTrace();
        }
    }
}

Option 2: @JsonIgnore property and register custom serializer/deserializer for root A class

SimpleModule module = new SimpleModule();
module.addSerializer(A.class, new ASerializer());
module.addDeserializer(A.class, new ADeserializer());
mapper.registerModule(module);

Can't use @JsonSerialize above A class, because serializers and deserializers inner ObjectMapper will also use this annotation, but you need to setup it to use default serializer/deserializer and not itself recursively. Or you could implement something like https://stackoverflow.com/a/18405958/1032167 if your really want the annotation

And serializer + deserializer would look something like this (Unoptimized and just a prove of concept):

    /************************ Serialization ************************/

public static class ASerializer extends JsonSerializer<A> {
    private static ObjectMapper m = new ObjectMapper();

    @Override
    public void serialize(A value, JsonGenerator gen,
                          SerializerProvider serializers) throws IOException {
        Map defaults = m.convertValue(value, Map.class);
        Map params = m.convertValue(value.getParameter(), Map.class);
        defaults.putAll(params);
        gen.writeObject(defaults);
    }

}

    /************************ Deserialization **********************/

public static class ADeserializer extends JsonDeserializer<A> {
    private static ObjectMapper m = new ObjectMapper();
    private static String[] subtipes = {"integerParam", "comboParam"};

    public ADeserializer() {
        m.disable(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES);
    }

    @Override
    public A deserialize(JsonParser p, DeserializationContext ctxt)
            throws IOException {

        TreeNode node = m.readTree(p);

        A a = m.convertValue(node, A.class);

        // hardcoded , probably can be done dynamically
        // with annotations inspection
        for (String key : subtipes) {
            TreeNode value = node.get(key);
            if (value != null) {
                String json = String.format("{\"%s\":%s}", key, value);
                a.setParameter(m.readValue(json, Parameter.class));
                break;
            }
        }

        return a;
    }
}

Generic deserializer would be pretty hard to write. But this question is about serialization anyway according to the question body.

like image 116
varren Avatar answered Oct 28 '22 04:10

varren