Search code examples
javajsonjacksonjackson-databind

@JsonIdentityInfo fails to deserialize object cross sections within the same file


I parse JSON object using Java ObjectMapper. Some of the objects should be reusable, therefore I'm using @JsonIdentityInfo to resolve references to them (I am able to define them inline or as reference). At first that worked.

However, I do have 2 types of objects, and they may refer to each other (there is inheritance there, and some objects are compound object).

My input file is separated into sections. Now, my problem is cross referencing between section. If the referenced object is read in the 1st section, it's fine. However, if the referenced object is in the 2nd section, it will not resolve that. I do not want to enforce order, as there may be cross references.

Is there a way to force ObjecMapper to look at the entire input before failing?

For example, these are my classes (very simplified of my use case):

@Getter
@Setter
@JsonTypeInfo(
    use = JsonTypeInfo.Id.NAME,
    property = "@type",
    visible = true)
@JsonSubTypes({
    @JsonSubTypes.Type(value = AValue.class, name = "AValue"),
    @JsonSubTypes.Type(value = AReferenceToB.class, name = "AReferenceToB"),
})
@JsonIdentityInfo(scope = A.class, generator = ObjectIdGenerators.PropertyGenerator.class)
public abstract class A {
    @JsonProperty("@id")
    private String id;
    @JsonProperty("@type")
    private String type;
}

@Getter
@Setter
public class AValue extends A {
    @NonNull
    String value;

    @JsonCreator
    public AValue(@JsonProperty(value = "value", required = true) @NonNull String value) {
        this.value = value;
    }
}

@Getter
@Setter
public class AReferenceToB extends A{
    @NonNull
    B b;

    @JsonCreator
    public AReferenceToB(@JsonProperty(value = "b", required = true) @NonNull B b) {
        this.b = b;
    }
}

@Getter
@Setter
@JsonTypeInfo(
    use = JsonTypeInfo.Id.NAME,
    property = "@type",
    visible = true)
@JsonSubTypes({
    @JsonSubTypes.Type(value = BValue.class, name = "BValue"),
    @JsonSubTypes.Type(value = BReferenceToA.class, name = "BReferenceToA"),
})
@JsonIdentityInfo(scope = B.class, generator = ObjectIdGenerators.PropertyGenerator.class)
public class B {
    @JsonProperty("@id")
    private String id;
    @JsonProperty("@type")
    private String type;
}

@Getter
@Setter
public class BValue extends B {
    int value;

    @JsonCreator
    public BValue(@JsonProperty(value = "value", required = true) int value) {
        this.value = value;
    }
}

@Getter
@Setter
public class BReferenceToA extends B {
    @NonNull
    A a;

    @JsonCreator
    public BReferenceToA(@JsonProperty(value = "a", required = true) @NonNull A a) {
        this.a = a;
    }
}

And this is the class that I read the JSON into:

@Getter
@Setter
public class File {
    @NonNull
    private List<A> as;

    @NonNull
    private List<B> bs;

    @JsonCreator
    public File(@JsonProperty(value = "as", required = true) @NonNull List<A> as,
                @JsonProperty(value = "bs", required = true) @NonNull List<B> bs) {
        this.as = as;
        this.bs = bs;
    }
}

Reading this file succeeds with out the a2, but will fail with a2. It will not work if I swap as and bs as well.

{
    "as": [
        {
            "@id": "a1",
            "@type": "AValue",
            "value": "test"
        },
        {
            "@id": "a2",
            "@type": "AReferenceToB",
            "b": "b1"
        }

    ],
    "bs": [
        {
            "@id": "b1",
            "@type": "BValue",
            "value": 1
        },
        {
            "@id": "b2",
            "@type": "BReferenceToA",
            "a": "a1"
        }

    ]
}

Failure:

com.fasterxml.jackson.databind.exc.InvalidTypeIdException: Could not resolve subtype of [simple type, class A]: missing type id property '@type' (for POJO property 'as')
 at [Source: (File); line: 12, column: 9] (through reference chain: File["as"]->java.util.ArrayList[1])

Code:

ObjectMapper mapper = new ObjectMapper();
final File file = mapper.readValue(Paths.get("test.json").toFile(), File.class);

Solution

  • Introduction

    Let's consider the following subset of the Maven dependencies as the current version of the Jackson library:

    <dependency>
        <groupId>com.fasterxml.jackson.core</groupId>
        <artifactId>jackson-core</artifactId>
        <version>2.13.2</version>
    </dependency>
    <dependency>
        <groupId>com.fasterxml.jackson.core</groupId>
        <artifactId>jackson-databind</artifactId>
        <version>2.13.2.2</version>
    </dependency>
    

    Answer

    It looks like currently such usage of @JsonProperty, @JsonIdentityInfo, and collection (the List<E> interface) is not supported by the Jackson library.

    Please, see the open GitHub issue: Correctly deserialize forward @JsonIdentityInfo references when using @JsonCreator · Issue #3030 · FasterXML/jackson-databind.

    Please, note the comment on the GitHub issue:

    cowtowncoder commented on Jan 30, 2021

    Correct: there may be cases with combination of @JsonCreator, identity info, and ordered collections (Lists and arrays) that may not be possible support ever, at all. Calling constructor is not possible without having actual object, and conversely values of collections must be deserialized in order. You may need to change the usage so that List properties in question are passed by setters or fields; or possibly use Builder-style if immutability is required (builders work as long as built type itself does not use object id; its properties can use them).

    Workarounds

    Some workarounds are described in the already mentioned comment on the GitHub issue.

    Example workaround: Use field instead of constructor for deserialization

    The example works fine, after updating the AReferenceToB class to use the field instead of the constructor for deserialization:

    @Getter
    @Setter
    public class AReferenceToB extends A {
        @JsonProperty(value = "b", required = true)
        @NonNull
        B b;
    }
    

    The disadvantages of the workaround:

    • It introduces mutability. The AReferenceToB class has become mutable.