JacksonPolymorphicDeserialization: JsonMappingException
Asked Answered
M

2

9

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

Mcgannon answered 11/3, 2015 at 17:11 Comment(7)
look for Jackson Polymorphic DeserializationGaily
you can find an example hereGaily
I edited the question to be clearerMcgannon
k, what is the problem? How you want result to be?Gaily
I need a result like that: { integerParam:{ regEx: regExVal } }Mcgannon
remove include = As.WRAPPER_OBJECT from Parmeter class and try.Gaily
The same problem persist...Mcgannon
D
3

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://mcmap.net/q/156331/-how-do-i-call-the-default-deserializer-from-a-custom-deserializer-in-jackson 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.

Dichlorodiphenyltrichloroethane answered 17/10, 2017 at 23:23 Comment(0)
S
0

Maybe its too late , but still posting for anyone who comes looking. I managed to solve this using controlled serialization with @JsonAnyGetter. And polymorphic deserialization using multiple setters having different @JsonProperty value attributes.

class A {
   Parameter parameter;

   @JsonProperty("integerParam") /* DESERIALIZE if integerParam type */
   public void setParameter(IntegerParameter parameter) {
       this.parameter = parameter;
   }
   @JsonProperty("comboParam") /* DESERIALIZE if comboParam type */
   public void setParameter(ComboParameter parameter) {
       this.parameter = parameter;
   }
   @JsonIgnore /*prevent serialization using default getter, this will wrap stuff into parameter*/
   public Parameter getParameter() {
       return parameter;
   }
   @JsonAnyGetter/*SERIALIZE: use any getter to add custom wrapper as per the child class type*/
   private Map<String, Object> parameterAsMap(){
       if (this.parameter instanceof IntegerParameter)
           return Map.of("integerParam",this.parameter);
       else
           return Map.of("comboParam",this.parameter);
   }
}

All the type and subtypes classes will have no Type information i.e.

abstract class Parameter {
    String regEx;
}
class IntegerParameter extends Parameter {
}
class ComboParameter extends Parameter {
    List<String> values;
}

finally we get unwrapped Jsons as following:

{
  "integerParam" : {
    "regEx" : "regExIntVal"
  }
}

AND

{
  "comboParam" : {
    "regEx" : "regExComboVal",
    "values" : [ "X", "Y", "Z" ]
  }
}
Sanburn answered 23/12, 2023 at 14:18 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.