jackson-databind icon indicating copy to clipboard operation
jackson-databind copied to clipboard

Correctly deserialize forward `@JsonIdentityInfo` references when using `@JsonCreator`

Open benediktsatalia opened this issue 3 years ago • 3 comments

Is your feature request related to a problem? Please describe. Currently we cannot use forward references together with a @JsonCreator. See the following example:

class A1 {
  public List<B> bs;
  public List<C1> cs;
}

@JsonIdentityInfo(generator = ObjectIdGenerators.PropertyGenerator.class, property = "id")
class B {
  public String id;
}

class C1 {
  private B b;

  @JsonCreator
  public C1(@JsonProperty("b") B b) {
    this.b = b;
  }

  @JsonGetter("b")
  public B getB() {
    return b;
  }
}

class C2 {
  public B b;
}

class A2 {
  public List<B> bs;
  public List<C2> cs;
}

public class Main {

  public static void main(final String[] args) throws IOException {
    ObjectMapper mapper = new ObjectMapper();

    A1 a2 = mapper.readValue("{\"bs\":[{\"id\":\"b1\"},{\"id\":\"b2\"}],\"cs\":[{\"b\":\"b1\"},{\"b\":\"b2\"}]}", A1.class);
    System.out.println("Using correct ordered data (no forward references) with JsonCreator works");
    A2 a3 = mapper.readValue("{\"cs\":[{\"b\":\"b1\"},{\"b\":\"b2\"}],\"bs\":[{\"id\":\"b1\"},{\"id\":\"b2\"}]}", A2.class);
    System.out.println("Using incorrect ordered data (with forward references) with no JsonCreator works");
    A1 a4 = mapper.readValue("{\"cs\":[{\"b\":\"b1\"},{\"b\":\"b2\"}],\"bs\":[{\"id\":\"b1\"},{\"id\":\"b2\"}]}", A1.class);
  }
}

The first two deserializations work as expected but the third one, where we use forward references and the @JsonCreator throws an exception.

This is the output when running the above code snippet:

Using correct ordered data (no forward references) with JsonCreator works
Using incorrect ordered data (with forward references) with no JsonCreator works
Exception in thread "main" com.fasterxml.jackson.databind.deser.UnresolvedForwardReference: Could not resolve Object Id [b1] (for [simple type, class B]).
 at [Source: (String)"{"cs":[{"b":"b1"},{"b":"b2"}],"bs":[{"id":"b1"},{"id":"b2"}]}"; line: 1, column: 17] (through reference chain: A1["cs"]->java.util.ArrayList[0]->C1["b"])
	at com.fasterxml.jackson.databind.deser.BeanDeserializerBase.deserializeFromObjectId(BeanDeserializerBase.java:1292)
	at com.fasterxml.jackson.databind.deser.BeanDeserializerBase.deserializeFromString(BeanDeserializerBase.java:1381)
	at com.fasterxml.jackson.databind.deser.BeanDeserializer._deserializeOther(BeanDeserializer.java:176)
	at com.fasterxml.jackson.databind.deser.BeanDeserializer.deserialize(BeanDeserializer.java:166)
	at com.fasterxml.jackson.databind.deser.SettableBeanProperty.deserialize(SettableBeanProperty.java:542)
	at com.fasterxml.jackson.databind.deser.BeanDeserializer._deserializeWithErrorWrapping(BeanDeserializer.java:535)
	at com.fasterxml.jackson.databind.deser.BeanDeserializer._deserializeUsingPropertyBased(BeanDeserializer.java:419)
	at com.fasterxml.jackson.databind.deser.BeanDeserializerBase.deserializeFromObjectUsingNonDefault(BeanDeserializerBase.java:1310)
	at com.fasterxml.jackson.databind.deser.BeanDeserializer.deserializeFromObject(BeanDeserializer.java:331)
	at com.fasterxml.jackson.databind.deser.BeanDeserializer.deserialize(BeanDeserializer.java:164)
	at com.fasterxml.jackson.databind.deser.std.CollectionDeserializer.deserialize(CollectionDeserializer.java:285)
	at com.fasterxml.jackson.databind.deser.std.CollectionDeserializer.deserialize(CollectionDeserializer.java:244)
	at com.fasterxml.jackson.databind.deser.std.CollectionDeserializer.deserialize(CollectionDeserializer.java:27)
	at com.fasterxml.jackson.databind.deser.impl.FieldProperty.deserializeAndSet(FieldProperty.java:138)
	at com.fasterxml.jackson.databind.deser.BeanDeserializer.vanillaDeserialize(BeanDeserializer.java:293)
	at com.fasterxml.jackson.databind.deser.BeanDeserializer.deserialize(BeanDeserializer.java:156)
	at com.fasterxml.jackson.databind.ObjectMapper._readMapAndClose(ObjectMapper.java:4482)
	at com.fasterxml.jackson.databind.ObjectMapper.readValue(ObjectMapper.java:3434)
	at com.fasterxml.jackson.databind.ObjectMapper.readValue(ObjectMapper.java:3402)
	at Main.main(Main.java:52)

Describe the solution you'd like Lazily call @JsonCreator for such objects only after the whole json (and all objects with ids) got read.

Usage example See my example above.

benediktsatalia avatar Jan 28 '21 12:01 benediktsatalia

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).

cowtowncoder avatar Jan 29 '21 23:01 cowtowncoder

Dear Tatu Saloranta (@cowtowncoder),

Just for your information. A related Stack Overflow question that contains a minimal, complete, and verifiable example: java - @JsonIdentityInfo fails to deserialize object cross sections within the same file - Stack Overflow.

Best regards, Sergey Vyacheslavovich Brunov.

svbrunov avatar Apr 13 '22 18:04 svbrunov

Thank you @svbrunov !

cowtowncoder avatar Apr 19 '22 16:04 cowtowncoder