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);
CodePudding user response:
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 (List
s 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 thatList
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.