Deserializing polymorphic types with Jackson based on the presence of a unique property
Asked Answered
W

8

63

If I have a class structure like so:

public abstract class Parent {
    private Long id;
    ...
}

public class SubClassA extends Parent {
    private String stringA;
    private Integer intA;
    ...
}

public class SubClassB extends Parent {
    private String stringB;
    private Integer intB;
    ...
}

Is there an alternative way to deserialize different then @JsonTypeInfo? Using this annotation on my parent class:

@JsonTypeInfo(use = JsonTypeInfo.Id.NAME, property = "objectType")

I would rather not have to force clients of my API to include "objectType": "SubClassA" to deserialize a Parent subclass.

Instead of using @JsonTypeInfo, does Jackson provide a way to annotate a subclass and distinguish it from other subclasses via a unique property? In my example above, this would be something like, "If a JSON object has "stringA": ... deserialize it as SubClassA, if it has "stringB": ... deserialize it as SubClassB".

Wait answered 17/6, 2014 at 12:31 Comment(0)
I
30

This feels like something @JsonTypeInfo and @JsonSubTypes should be used for but I've picked through the docs and none of the properties that can be supplied quite seem to match what you're describing.

You could write a custom deserializer that uses @JsonSubTypes' "name" and "value" properties in a non-standard way to accomplish what you want. The deserializer and @JsonSubTypes would be supplied on your base class and the deserializer would use the "name" values to check for the presence of a property and if it exists, then deserialize the JSON into the class supplied in the "value" property. Your classes would then look something like this:

@JsonDeserialize(using = PropertyPresentDeserializer.class)
@JsonSubTypes({
        @Type(name = "stringA", value = SubClassA.class),
        @Type(name = "stringB", value = SubClassB.class)
})
public abstract class Parent {
    private Long id;
    ...
}

public class SubClassA extends Parent {
    private String stringA;
    private Integer intA;
    ...
}

public class SubClassB extends Parent {
    private String stringB;
    private Integer intB;
    ...
}
Isometropia answered 17/6, 2014 at 13:40 Comment(2)
The PropertyPresentDeserializer seems to be a good thing. However, it does not seem to be included in Jackson. See GitHub search results: github.com/…Cavafy
@Cavafy It's a placeholder name for the custom deserializer I recommended the OP create.Isometropia
A
57

Here's a solution I've come up with that expands a bit on Erik Gillespie's. It does exactly what you asked for and it worked for me.

Using Jackson 2.9

@JsonDeserialize(using = CustomDeserializer.class)
public abstract class BaseClass {

    private String commonProp;
}

// Important to override the base class' usage of CustomDeserializer which produces an infinite loop
@JsonDeserialize(using = JsonDeserializer.None.class)
public class ClassA extends BaseClass {
    
    private String classAProp;
}

@JsonDeserialize(using = JsonDeserializer.None.class)
public class ClassB extends BaseClass {
    
    private String classBProp;
}

public class CustomDeserializer extends StdDeserializer<BaseClass> {

    protected CustomDeserializer() {
        super(BaseClass.class);
    }

    @Override
    public BaseClass deserialize(JsonParser p, DeserializationContext ctxt) throws IOException, JsonProcessingException {
        TreeNode node = p.readValueAsTree();
        
        // Select the concrete class based on the existence of a property
        if (node.get("classAProp") != null) {
            return p.getCodec().treeToValue(node, ClassA.class);
        }
        return p.getCodec().treeToValue(node, ClassB.class);
    }
}

// Example usage
String json = ...
ObjectMapper mapper = ...
BaseClass instance = mapper.readValue(json, BaseClass.class);

If you want to get fancier, you can expand CustomDeserializer to include a Map<String, Class<?>> that maps a property name that, when present, maps to a specific class. Such an approach is presented in this article.

Update

Jackson 2.12.0 gets Polymorphic subtype deduction from available fields which adds @JsonTypeInfo(use = DEDUCTION)!

AsDeductionTypeDeserializer implements inferential deduction of a subtype from the fields. As a POC not intended for merging, tere's an amount of cut'n'paste code etc but I thought a functional PR would be the best basis for discussion of something I write out of interest.

It works by fingerprinting the full set of possible fields of each subtype on registration. On deserialisation, available fields are compared to those fingerprints until only one candidate remains. It specifically only looks at immediate-child field names as is immediate-child values are covered by existing mechanisms and deeper analysis is a much more imposing ML task not really part of Jackson's remit.

By the way, there's a (now closed) Github issue requesting this here: https://github.com/FasterXML/jackson-databind/issues/1627

Adjuvant answered 25/4, 2018 at 2:15 Comment(1)
Your original answer is an easy workaround for me until I upgrade to Spring Boot 2.5.x so I can leverage the new Deduction-based Polymorphism from Jackson 2.12. Thank you.Tier
I
30

This feels like something @JsonTypeInfo and @JsonSubTypes should be used for but I've picked through the docs and none of the properties that can be supplied quite seem to match what you're describing.

You could write a custom deserializer that uses @JsonSubTypes' "name" and "value" properties in a non-standard way to accomplish what you want. The deserializer and @JsonSubTypes would be supplied on your base class and the deserializer would use the "name" values to check for the presence of a property and if it exists, then deserialize the JSON into the class supplied in the "value" property. Your classes would then look something like this:

@JsonDeserialize(using = PropertyPresentDeserializer.class)
@JsonSubTypes({
        @Type(name = "stringA", value = SubClassA.class),
        @Type(name = "stringB", value = SubClassB.class)
})
public abstract class Parent {
    private Long id;
    ...
}

public class SubClassA extends Parent {
    private String stringA;
    private Integer intA;
    ...
}

public class SubClassB extends Parent {
    private String stringB;
    private Integer intB;
    ...
}
Isometropia answered 17/6, 2014 at 13:40 Comment(2)
The PropertyPresentDeserializer seems to be a good thing. However, it does not seem to be included in Jackson. See GitHub search results: github.com/…Cavafy
@Cavafy It's a placeholder name for the custom deserializer I recommended the OP create.Isometropia
C
16

As other have pointed out, there's no consensus on how it should work so it hasn't been implemented.

If you have classes Foo, Bar and their parent FooBar solution seems pretty obvious when you have JSONs like:

{
  "foo":<value>
}

or

{
  "bar":<value>
}

but there's no common answer to what happens when you get

{
  "foo":<value>,
  "bar":<value>
}

At first glance last example seems like an obvious case of 400 Bad Request but there are many different approaches in practice:

  1. Handle it as 400 Bad Request
  2. Precedence by type/field (for example if field error exists it has higher precedence than some other field foo)
  3. More complex cases of 2.

My current solution which works for most cases and tries to leverage as much of existing Jackson infrastructure as possible is (you only need 1 deserializer per hierarchy):

public class PresentPropertyPolymorphicDeserializer<T> extends StdDeserializer<T> {

    private final Map<String, Class<?>> propertyNameToType;

    public PresentPropertyPolymorphicDeserializer(Class<T> vc) {
        super(vc);
        this.propertyNameToType = Arrays.stream(vc.getAnnotation(JsonSubTypes.class).value())
                                        .collect(Collectors.toMap(Type::name, Type::value,
                                                                  (a, b) -> a, LinkedHashMap::new)); // LinkedHashMap to support precedence case by definition order
    }

    @Override
    public T deserialize(JsonParser p, DeserializationContext ctxt) throws IOException {
        ObjectMapper objectMapper = (ObjectMapper) p.getCodec();
        ObjectNode object = objectMapper.readTree(p);
        for (String propertyName : propertyNameToType.keySet()) {
            if (object.has(propertyName)) {
                return deserialize(objectMapper, propertyName, object);
            }
        }

        throw new IllegalArgumentException("could not infer to which class to deserialize " + object);
    }

    @SuppressWarnings("unchecked")
    private T deserialize(ObjectMapper objectMapper,
                          String propertyName,
                          ObjectNode object) throws IOException {
        return (T) objectMapper.treeToValue(object, propertyNameToType.get(propertyName));
    }
}

Example usage:

@JsonSubTypes({
        @JsonSubTypes.Type(value = Foo.class, name = "foo"),
        @JsonSubTypes.Type(value = Bar.class, name = "bar"),
})
interface FooBar {
}
@AllArgsConstructor(onConstructor_ = @JsonCreator)
@Value
static class Foo implements FooBar {
    private final String foo;
}
@AllArgsConstructor(onConstructor_ = @JsonCreator)
@Value
static class Bar implements FooBar {
    private final String bar;
}

Jackson configuration

SimpleModule module = new SimpleModule();
module.addDeserializer(FooBar.class, new PresentPropertyPolymorphicDeserializer<>(FooBar.class));
objectMapper.registerModule(module);

or if you're using Spring Boot:

@JsonComponent
public class FooBarDeserializer extends PresentPropertyPolymorphicDeserializer<FooBar> {

    public FooBarDeserializer() {
        super(FooBar.class);
    }
}

Tests:

    @Test
    void shouldDeserializeFoo() throws IOException {
        // given
        var json = "{\"foo\":\"foo\"}";

        // when
        var actual = objectMapper.readValue(json, FooBar.class);

        // then
        then(actual).isEqualTo(new Foo("foo"));
    }

    @Test
    void shouldDeserializeBar() throws IOException {
        // given
        var json = "{\"bar\":\"bar\"}";

        // when
        var actual = objectMapper.readValue(json, FooBar.class);

        // then
        then(actual).isEqualTo(new Bar("bar"));

    }

    @Test
    void shouldDeserializeUsingAnnotationDefinitionPrecedenceOrder() throws IOException {
        // given
        var json = "{\"bar\":\"\", \"foo\": \"foo\"}";

        // when
        var actual = objectMapper.readValue(json, FooBar.class);

        // then
        then(actual).isEqualTo(new Foo("foo"));
    }

EDIT: I've added support for this case and more in this project.

Casseycassi answered 25/3, 2019 at 7:58 Comment(1)
Very cool to think of supporting a precedence order like this.Wait
J
11

EDIT (2021-07-15) -- obsolete, see M. Justin's answer for the current state of things.

(original answer below)

No. Such a feature has been requested -- it could be called "type inference", or "implied type" -- but no one has come up with a workable general proposal for how this should work yet. It is easy to think of ways to support specific solutions to specific cases, but figuring out general solution is more difficult.

Jijib answered 17/6, 2014 at 21:5 Comment(3)
Not surprised, it seems to be somewhat of a popular topic in Jackson's how-to realm. I would avoid a pattern based around abstract classes for domain objects that need to be de/serialized if I had the choice-- seems overly complex for whatever benefit it may provide.Wait
@SamB. Right, things get pretty complex rather fast; and it's not surprising that no solution has emerged, while multiple developers have proposed the general idea...Jijib
It's been implemented in Jackson 2.12 using polymorphic type by deduction. I've expanded this into its own answer: https://mcmap.net/q/168567/-deserializing-polymorphic-types-with-jackson-based-on-the-presence-of-a-unique-propertyZoroaster
Z
11

This functionality has been added to Jackson 2.12 using "deduction-based polymorphism". To apply it to your case, just use @JsonTypeInfo(use=Id.DEDUCTION) alongside the full list of supported subtypes provided by @JsonSubTypes:

@JsonTypeInfo(use=Id.DEDUCTION)
@JsonSubTypes({@Type(SubClassA.class), @Type(SubClassB.class)})
public abstract class Parent {
    private Long id;
    ...
}

This feature was implemented per jackson-databind#43, and is summarized in the 2.12 release notes:

It basically allows omitting of actual Type Id field or value, as long as the subtype can be deduced (@JsonTypeInfo(use=DEDUCTION)) from existence of fields. That is, every subtype has a distinct set of fields they included, and so during deserialization type can be uniquely and reliably detected.

A slightly longer explanation is given in the Jackson 2.12 Most Wanted (1/5): Deduction-Based Polymorphism article written by the Jackson creator.

Zoroaster answered 12/2, 2021 at 6:53 Comment(0)
A
6

My app requires me to preserve old structure, so I found a way to support polymorphism without changing data. Here's what I do:

  1. Extend JsonDeserializer
  2. Convert to Tree and read field, then return Subclass object

    @Override public Object deserialize(JsonParser p, DeserializationContext ctxt) throws IOException {
        JsonNode jsonNode = p.readValueAsTree(); 
        Iterator<Map.Entry<String, JsonNode>> ite = jsonNode.fields();
        boolean isSubclass = false;
        while (ite.hasNext()) {
            Map.Entry<String, JsonNode> entry = ite.next();
            // **Check if it contains field name unique to subclass**
            if (entry.getKey().equalsIgnoreCase("Field-Unique-to-Subclass")) {
                isSubclass = true;
                break;
            }
        }
        if (isSubclass) {
            return mapper.treeToValue(jsonNode, SubClass.class);
        } else {
            // process other classes
        }
    }
    
Ambi answered 24/2, 2016 at 10:32 Comment(0)
B
1

Have seen couple of answers above but i would like to add mine since this was used in production for last 2 years.

We have a EventTypes of Speed, Location for a vehicle.
Dynamic binding could be achieved with @JsonTypeInfo

@JsonTypeInfo(
        use = JsonTypeInfo.Id.NAME,
        include = JsonTypeInfo.As.PROPERTY,
        property = "eventId")
@JsonSubTypes({
        @JsonSubTypes.Type(value = SpeedEvent.class, name = "10"),
        @JsonSubTypes.Type(value = LocationEvent.class, name = "11")
})
public interface EventDetails {
}
public class Event {

    private int eventId;
    private long ts;
    private EventDetails eventDetails;

    public int getEventId() {
        return eventId;
    }

    public void setEventId(int eventId) {
        this.eventId = eventId;
    }

    public long getTs() {
        return ts;
    }

    public void setTs(long ts) {
        this.ts = ts;
    }

    public EventDetails getEventDetails() {
        return eventDetails;
    }

    public void setEventDetails(EventDetails eventDetails) {
        this.eventDetails = eventDetails;
    }
}
public class SpeedEvent implements EventDetails {

    private int eventId;
    private int speed;

    public int getEventId() {
        return eventId;
    }

    public void setEventId(int eventId) {
        this.eventId = eventId;
    }

    public int getSpeed() {
        return speed;
    }

    public void setSpeed(int speed) {
        this.speed = speed;
    }
}
public class LocationEvent implements EventDetails {

    private int eventId;
    private double latitude;
    private double longitude;

    public int getEventId() {
        return eventId;
    }

    public void setEventId(int eventId) {
        this.eventId = eventId;
    }

    public double getLatitude() {
        return latitude;
    }

    public void setLatitude(double latitude) {
        this.latitude = latitude;
    }

    public double getLongitude() {
        return longitude;
    }

    public void setLongitude(double longitude) {
        this.longitude = longitude;
    }
}

Sample Output:-
Speed Event

{
    "eventId": 10,
    "ts": 1684937053366,
    "eventDetails": {
        "eventId": "10",
        "speed": 50
    }
}

Location Event

{
    "eventId": 11,
    "ts": 1684937172450,
    "eventDetails": {
        "eventId": "11",
        "latitude": 41.40338,
        "longitude": 2.17403
    }
}
Barabarabarabas answered 24/5, 2023 at 13:59 Comment(0)
B
0

Handling polymorphism is either model-bound or requires lots of code with various custom deserializers. I'm a co-author of a JSON Dynamic Deserialization Library that allows for model-independent json deserialization library. The solution to OP's problem can be found below. Note that the rules are declared in a very brief manner.

public class SOAnswer1 {

    @ToString @Getter @Setter
    @AllArgsConstructor @NoArgsConstructor
    public static abstract class Parent {
        private Long id;
    }

    @ToString(callSuper = true) @Getter @Setter
    @AllArgsConstructor @NoArgsConstructor
    public static class SubClassA extends Parent {
        private String stringA;
        private Integer intA;
    }

    @ToString(callSuper = true) @Getter @Setter
    @AllArgsConstructor @NoArgsConstructor
    public static class SubClassB extends Parent {
        private String stringB;
        private Integer intB;
    }

    public static void main(String[] args) {
        String json = "[{"
                + "    \"id\": 151243,"
                + "    \"stringA\": \"my special string\","
                + "    \"intA\": 1337"
                + "},"
                + "{"
                + "    \"id\": 734561,"
                + "    \"stringB\": \"use the Force\","
                + "    \"intB\": 451"
                + "}]";
        
        // create a deserializer instance
        DynamicObjectDeserializer deserializer = new DynamicObjectDeserializer();
        
        // runtime-configure deserialization rules
        deserializer.addRule(DeserializationRuleFactory.newRule(1, 
                (e) -> e.getJsonNode().has("stringA"),
                DeserializationActionFactory.objectToType(SubClassA.class)));
        
        deserializer.addRule(DeserializationRuleFactory.newRule(1, 
                (e) -> e.getJsonNode().has("stringB"),
                DeserializationActionFactory.objectToType(SubClassB.class)));
        
        List<Parent> deserializedObjects = deserializer.deserializeArray(json, Parent.class);
        
        for (Parent obj : deserializedObjects) {
            System.out.println("Deserialized Class: " + obj.getClass().getSimpleName()+";\t value: "+obj.toString());
        }
    }
}

output:

Deserialized Class: SubClassA;   value: SOAnswer1.SubClassA(super=SOAnswer1.Parent(id=151243), stringA=my special string, intA=1337)
Deserialized Class: SubClassB;   value: SOAnswer1.SubClassB(super=SOAnswer1.Parent(id=734561), stringB=use the Force, intB=451)

Maven depenendency for pretius-jddl (check newest version at maven.org/jddl:

<dependency>
  <groupId>com.pretius</groupId>
  <artifactId>jddl</artifactId>
  <version>1.0.0</version>
</dependency>
Beet answered 21/12, 2020 at 9:5 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.