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

Unresolved forward reference (since 2.9.6)

Open Chris-Lercher opened this issue 5 years ago • 19 comments

Since upgrading from jackson version 2.9.5 to 2.9.6, we get an UnresolvedForwardReference Exception:

"com.fasterxml.jackson.databind.deser.UnresolvedForwardReference: Unresolved forward references for ..."

I have double-checked this by going back and forth between versions 2.8.11, 2.9.0, 2.9.5, 2.9.6 and 2.9.8. The issue appears since 2.9.6.

Our mappings contain MixIns like:

    @JsonTypeInfo(use = JsonTypeInfo.Id.NONE)
    @Entity
    public class RefResolvingBarMixIn extends Bar {

        @JsonIdentityInfo(generator = ObjectIdGenerators.PropertyGenerator.class, 
                property = "id", scope = Customer.class)
        @JsonIdentityReference(alwaysAsId = true)
        @Override
        public Customer getCustomer() {
            return super.getCustomer();
        }
    }

Our ObjectMapper setup is basically:

final ObjectMapper objectMapper = new ObjectMapper();
objectMapper.configure(SerializationFeature.FAIL_ON_EMPTY_BEANS, false);
objectMapper.configure(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES, false);

objectMapper.registerModule(new SimpleModule() {
    @Override
    public void setupModule(SetupContext context) {
        super.setupModule(context);
        context.setMixInAnnotations(Bar.class, RefResolvingBarMixIn.class);
        ...
    }
);
final SimpleModule refResolvingModule = new SimpleModule();
refResolvingModule.addDeserializer(Customer.class, new JsonDeserializer<Customer>() {
    @Override
    public Customer deserialize(JsonParser p, DeserializationContext ctxt) 
              throws IOException {
        final long id = p.getLongValue();
        final Customer customer = ...;
        Preconditions.checkState(customer != null, 
                  "Cannot find referenced customer with id " + id);
        return customer;
    }
});
objectMapper.registerModule(refResolvingModule);

The parsed JSON contains the Bar object with the customer correctly serialized as a numeric id

    {
       "..." : "...",
       "customer" : 1
    }

(Note: Our deserializer above is able find and return the Customer with id 1, so no problem there, and the code works fine on the same JSON data with Jackson <= 2.9.5.)

Chris-Lercher avatar Feb 04 '19 16:02 Chris-Lercher

Ok. I would need a full reproduction to trigger the problem, description is not enough since there are many existing tests for the feature. Use of mix-in is not necessarily related, but custom deserializer may well be.

cowtowncoder avatar Feb 04 '19 22:02 cowtowncoder

It took me some time to reproduce this issue, mostly because I was looking a bit in the wrong spot. As a matter of fact, it has nothing to do with MixIns - and moreover, it even has nothing to do with resolvers. The following test works with jackson-databind <= 2.9.5, but fails in version >= 2.9.6 with the mentioned UnresolvedForwardReference Exception:

import com.fasterxml.jackson.annotation.JsonIdentityInfo;
import com.fasterxml.jackson.annotation.JsonIdentityReference;
import com.fasterxml.jackson.annotation.ObjectIdGenerators;
import com.fasterxml.jackson.core.JsonProcessingException;
import com.fasterxml.jackson.databind.ObjectMapper;
import org.junit.Before;
import org.junit.Test;

import java.io.IOException;
import java.util.Map;
import java.util.TreeMap;

import static org.junit.Assert.assertEquals;

public class Issue2245Test {

    /**
     * Customer
     */
    public static class Customer {

        private Long id;

        public Long getId() {
            return id;
        }
        public void setId(Long id) {
            this.id = id;
        }
    }

    /**
     * Bar
     */
    public static class Bar {

        private Long id;
        private Customer customer;

        public Long getId() {
            return id;
        }
        public void setId(Long id) {
            this.id = id;
        }

        @JsonIdentityInfo(generator = ObjectIdGenerators.PropertyGenerator.class, property = "id", scope = Customer.class)
        @JsonIdentityReference(alwaysAsId = true)
        public Customer getCustomer() {
            return customer;
        }
        public void setCustomer(Customer customer) {
            this.customer = customer;
        }
    }

    /**
     * Model
     */
    public static class Model {
        private Map<Long, Customer> customers;

        private Bar bar;

        public Map<Long, Customer> getCustomers() {
            return customers;
        }
        public void setCustomers(Map<Long, Customer> customers) {
            this.customers = customers;
        }


        public Bar getBar() {
            return bar;
        }

        public void setBar(final Bar bar) {
            this.bar = bar;
        }
    }



    private String json;

    @Before
    public void before() throws JsonProcessingException {
        final Model model = new Model();

        final Customer customer = new Customer();
        customer.setId(1L);

        final Map<Long, Customer> customers = new TreeMap<>();
        customers.put(customer.getId(), customer);

        model.setCustomers(customers);

        final Bar bar = new Bar();
        bar.setId(1000L);
        bar.setCustomer(customer);

        model.setBar(bar);

        final ObjectMapper objectMapper = new ObjectMapper();
        json = objectMapper.writeValueAsString(model);
    }

    @Test
    public void test() throws IOException {

        assertEquals(
             "{'customers':{'1':{'id':1}},'bar':{'id':1000,'customer':1}}"
                    .replace("'", "\""), 
             json);
        
        final ObjectMapper objectMapper = new ObjectMapper();
        final Model model = objectMapper.readValue(json, Model.class);

        assertEquals(1L, model.getBar().getCustomer().getId().longValue());
    }
}

Chris-Lercher avatar Feb 21 '19 16:02 Chris-Lercher

Thank you for reproduction. That makes it possible to at some point figure out what is going on.

cowtowncoder avatar Feb 21 '19 23:02 cowtowncoder

I am facing the same problem. It works as long as I use the @JsonIdentityInfo on class level. For some reasons, I need to use this annotation on property level together with @JsonIdentityReference(alwaysAsId = true).

Serializing works, but deserializing fails with the UnresolvedForwardReference error. None of my object ids is picked up during parsing.

panzerj avatar Apr 12 '19 10:04 panzerj

Hi, guys. Maybe following information will be useful. Using git bisect and test class provided by @Chris-Lercher , I found commit which caused this issue. https://github.com/FasterXML/jackson-databind/commit/a3bd2f55fdc240 This commit make BeanPropertyMap immutable.

As I understand issue appears because there is a caching mechanism of deserializers and previously they can share an instance of BeanPropertyMap and since there was possibility to modify beanPropertyMap, deserializers always get updated beanPropertyMap and everything worked as expected.

Issue was fixed in current master(commit 'e370d3296890561a88c3be48733cbe59f67431ca') and still exist in 2.10 branch @cowtowncoder Is there any chance to fix it in nearest future?

ystefanyshyn avatar Jul 04 '19 07:07 ystefanyshyn

@ystefanyshyn Making BeanPropertyMap immutable is a feature and I don't think that should change.

But I would definitely be interested in learning what fix in master could help here, if any. Code is quite different at this point, and some of changes are not easy to port.

I haven't had time to work on this issue, and probably won't in near future. But I hope this extra information might help someone else dig deeper.

cowtowncoder avatar Jul 18 '19 23:07 cowtowncoder

Any Progress on this? Sticking to Version 2.9.5 starts getting complicated

siemensoe avatar Mar 02 '20 13:03 siemensoe

I haven't had time to look into this unfortunately.

cowtowncoder avatar Mar 03 '20 00:03 cowtowncoder

This works fine though - With the type reference in the correct place? @panzerj what scenario exists where the declarative ID for the type it is forced on the method? The below shows it working according to the documentation with the serialization in place

 /**
     * Customer
     */
    @JsonIdentityInfo(generator = ObjectIdGenerators.PropertyGenerator.class, property = "id", scope = Customer.class)
    public static class Customer {

        private Long id;

        public Long getId() {
            return id;
        }
        public void setId(Long id) {
            this.id = id;
        }
    }

    /**
     * Bar
     */

    public static class Bar {

        private Long id;
        private Customer customer;

        public Long getId() {
            return id;
        }
        public void setId(Long id) {
            this.id = id;
        }

        @JsonIdentityReference(alwaysAsId = true)
        public Customer getCustomer() {
            return customer;
        }
        public void setCustomer(Customer customer) {
            this.customer = customer;
        }
    }

GedMarc avatar Mar 03 '20 08:03 GedMarc

I have a complex data structure and I need to have full control over the places where Objects are serialized by id and where not. I am converting the java data model to typescript for client usage and I need to know exactly whether to expect an object or a string(the id) I achieved that by removing @JsonIdentityInfo from class and only use @JsonIdentityInfo together with @JsonIdentityReference(alwaysAsId = true) on method level at places where I want to replace the object by its id. (JsonIdentityReference Annotation has METHOD and FIELD as targets) This stopped working with version 2.9.6...

With the @JsonIdentityInfo placed on the class - the converter cannot decide how the object is serialized.

siemensoe avatar Mar 03 '20 08:03 siemensoe

From what I can see, the above solution caters for this. The ID is only serialized on methods/fields where @JsonidentityReference is specified, and the type id is correctly specified on the class. The only situation I can see this being a problem is when an object has more than one ID that you want to reference separately for getters and field, leading to a mistake in the modelling.

See the fix I want to do is to remove the Field target for @JsonIdentityInfo, as it should only be available on a class level, as it denotes an ID for an object.

GedMarc avatar Mar 03 '20 08:03 GedMarc

When the TypeScriptConverter finds a field with the annotation @JsonidentityReference without (alwaysAsId = true) it cannot decide whether to use object or id as type because jackson serializes the first occurrence as object and subsequent occurrences as id. Even if I use the annotation only once - the json serializing/deserializing is fine, but the converter cannot decide because there is no opposite to alwaysAsId = true. I hope, I can express myself understandably

siemensoe avatar Mar 03 '20 08:03 siemensoe

@siemensoe Please see below attachment the example of it working as described (any further instances use the id instead of the object whether or not alwaysAsId is set)

This is solved for 2.10 unless something is wrong in the test case. @cowtowncoder can we limit the usage of @JsonIdentityReference to class only?

tests.zip

GedMarc avatar Mar 03 '20 12:03 GedMarc

But that´s exactly my problem. When I annotate my class with @JsonIdentityInfo the first instance is serialized as object and any further instances are serialized as id. Even if there is no @JsonidentityReference annotation on the field. If I look at a single field, I cannot tell whether it will be serialized as id or not. For the java side serialize/deserialize works, but if i use the json to communicate with a javascript client this makes things quite complicated. A property like alwaysAsObject would also help..

siemensoe avatar Mar 03 '20 13:03 siemensoe

@GedMarc Thanks for looking into this issue, and finding a solution that will probably work for us.

Thinking about it, having @JsonIdentityInfo on the getter probably has weird semantics anyway. I'm not so sure what would happen, if there was another class Foo which also references Customer but uses a @JsonIdentityInfo annotation with different parameters. So probably, not allowing this in the first place is the right thing to do.

There is, however, actually a situation which can not be covered by putting @JsonIdentityInfo on the class level, and now it actually has something to do with MixIns.

    public static class Customer {

        private Long id;
        private String dummy;

        public Long getId() {
            return id;
        }
        public void setId(Long id) {
            this.id = id;
        }

        public String getDummy() {
            return dummy;
        }

        public void setDummy(final String dummy) {
            this.dummy = dummy;
        }
    }

    @JsonIdentityInfo(generator = ObjectIdGenerators.PropertyGenerator.class, property = "id", scope = Customer.class)
    public static class CustomerMixIn extends Customer {

        @Override
        @JsonIgnore
        public String getDummy() {
            return super.getDummy();
        }
    }

(Of course, register the MixIn with objectMapper.addMixIn(Customer.class, CustomerMixIn.class); before using writeValueAsString).

This leads to an Exception

com.fasterxml.jackson.databind.exc.MismatchedInputException: Cannot construct instance of `Issue2245Test$Customer` (although at least one Creator exists): no int/Int-argument constructor/factory method to deserialize from Number value (1)
 at [Source: (String)"{"customers":{"1":{"id":1}},"bar":{"id":1000,"customer":1}}"; line: 1, column: 57] (through reference chain: Issue2245Test$Model["bar"]->Issue2245Test$Bar["customer"])

If one has the possibility to move @JsonIdentityInfo from CustomerMixIn to Customer, then there is no problem, but part of the reason for MixIns is to be able to override things in imported models which cannot be changed.

Do you think it would be possible to allow overriding class level annotations via MixIns?

Chris-Lercher avatar Mar 03 '20 15:03 Chris-Lercher

@GedMarc No, I don't think @JsonIdentityReference can be limited to class only: there are existing tests and I think the original use case was specifically to allow some cases to use alwaysAsId = true to prevent serialization as reference.

@Chris-Lercher I think mix-ins should allow overriding class annotations as well as member annotations (even if latter are more common).

Another thing to note -- not sure if it helps or not -- is that it would be possible to extend "config override" approach (mapper.configOverride(forClass).xxx()) to cover this aspect, to add programmatic override. Currently overrides are possible for:

  • @JsonInclude (null serialization)
  • @JsonIgnoreProperties
  • @JsonSetter (null deserialization)
  • @JsonAutoDetect (introspection settings)
  • @JsonIgnoreType
  • @JsonMerge

to basically add overrides that have precedence higher than class annotations; and settable programmatically. I have not read this whole thread so I don't know how applicable this is but thought it worth mentioning.

cowtowncoder avatar Mar 04 '20 17:03 cowtowncoder

Just hit the same issue with a structure similar like this: LegalEntity -- TransportInfo transportInfo ---- LegalEntity importer ---- LegalEntity exporter

LegalEntity is annotated with @JsonIdentityInfo(generator= ObjectIdGenerators.IntSequenceGenerator.class, scope = LegalEntity.class) on class level

Adding @JsonIdentityReference(alwaysAsId = true) on TransportInfo.exporter / importer does not change anything. Also creating an explicit id field didn't help.

For me the complete feature "@JsonIdentityReference" is rendered useless, is there a workaround? The only option i see is to manually generate id and change type of importer / exporter to type of id.

dermoritz avatar Jun 29 '20 08:06 dermoritz

We've hit the same issue, and the reason we can't use @JsonIdentityInfo on the class level, is that we have an entity that has both an id and a guid properties, and some children refer to it by id while others by guid. I realise this is not the best practice, but that's the situation we are in.

The @JsonIdentityInfo on properties used to work fine, until we upgraded to Spring Boot 2.3.3, which in turn upgraded Jackson to 2.11.2 - and then we started seeing this issue.

Unfortunately, we can't seem to downgrade only Jackson to 2.9.5 (which seems to be the last version that did not have this issue) while keeping Spring Boot at 2.3.3 (and in particular spring-kafka), because we then get this error:

Caused by: java.lang.NoClassDefFoundError: com/fasterxml/jackson/databind/json/JsonMapper
	at org.springframework.kafka.support.JacksonUtils.enhancedObjectMapper(JacksonUtils.java:58) ~[spring-kafka-2.5.5.RELEASE.jar:2.5.5.RELEASE]
	at org.springframework.kafka.support.JacksonUtils.enhancedObjectMapper(JacksonUtils.java:47) ~[spring-kafka-2.5.5.RELEASE.jar:2.5.5.RELEASE]
	at org.springframework.kafka.support.DefaultKafkaHeaderMapper.<init>(DefaultKafkaHeaderMapper.java:112) ~[spring-kafka-2.5.5.RELEASE.jar:2.5.5.RELEASE]
	at org.springframework.kafka.support.converter.MessagingMessageConverter.<init>(MessagingMessageConverter.java:67) ~[spring-kafka-2.5.5.RELEASE.jar:2.5.5.RELEASE]
	at org.springframework.kafka.core.KafkaTemplate.<init>(KafkaTemplate.java:101) ~[spring-kafka-2.5.5.RELEASE.jar:2.5.5.RELEASE]
	at org.springframework.kafka.core.KafkaTemplate.<init>(KafkaTemplate.java:150) ~[spring-kafka-2.5.5.RELEASE.jar:2.5.5.RELEASE]
	at org.springframework.kafka.core.KafkaTemplate.<init>(KafkaTemplate.java:122) ~[spring-kafka-2.5.5.RELEASE.jar:2.5.5.RELEASE]
	at com.foo.bar.config.kafka.KafkaProducerConfig.kafkaTemplate(KafkaProducerConfig.java:40) ~[classes/:na]
	at com.foo.bar.config.kafka.KafkaProducerConfig$$EnhancerBySpringCGLIB$$d8802e9c.CGLIB$kafkaTemplate$1(<generated>) ~[classes/:na]
	at com.foo.bar.config.kafka.KafkaProducerConfig$$EnhancerBySpringCGLIB$$d8802e9c$$FastClassBySpringCGLIB$$5bec15c4.invoke(<generated>) ~[classes/:na]
	at org.springframework.cglib.proxy.MethodProxy.invokeSuper(MethodProxy.java:244) ~[spring-core-5.2.8.RELEASE.jar:5.2.8.RELEASE]
	at org.springframework.context.annotation.ConfigurationClassEnhancer$BeanMethodInterceptor.intercept(ConfigurationClassEnhancer.java:331) ~[spring-context-5.2.8.RELEASE.jar:5.2.8.RELEASE]
	at com.foo.bar.config.kafka.KafkaProducerConfig$$EnhancerBySpringCGLIB$$d8802e9c.kafkaTemplate(<generated>) ~[classes/:na]
	at sun.reflect.NativeMethodAccessorImpl.invoke0(Native Method) ~[na:1.8.0_201]
	at sun.reflect.NativeMethodAccessorImpl.invoke(NativeMethodAccessorImpl.java:62) ~[na:1.8.0_201]
	at sun.reflect.DelegatingMethodAccessorImpl.invoke(DelegatingMethodAccessorImpl.java:43) ~[na:1.8.0_201]
	at java.lang.reflect.Method.invoke(Method.java:498) ~[na:1.8.0_201]
	at org.springframework.beans.factory.support.SimpleInstantiationStrategy.instantiate(SimpleInstantiationStrategy.java:154) ~[spring-beans-5.2.8.RELEASE.jar:5.2.8.RELEASE]
	... 73 common frames omitted
Caused by: java.lang.ClassNotFoundException: com.fasterxml.jackson.databind.json.JsonMapper
	at java.net.URLClassLoader.findClass(URLClassLoader.java:382) ~[na:1.8.0_201]
	at java.lang.ClassLoader.loadClass(ClassLoader.java:424) ~[na:1.8.0_201]
	at sun.misc.Launcher$AppClassLoader.loadClass(Launcher.java:349) ~[na:1.8.0_201]
	at java.lang.ClassLoader.loadClass(ClassLoader.java:357) ~[na:1.8.0_201]
	... 91 common frames omitted

Any pointers/workarounds appreciated.

austalakov avatar Sep 22 '20 15:09 austalakov

I found a workaround for my issue with the typescript converter:

I only annotate the properties which should be converted to the id with: @JsonIdentityInfo(generator = ObjectIdGenerators.PropertyGenerator.class, property = "id") @JsonIdentityReference(alwaysAsId = true)

Instead on annotating the Class with @JsonIdentityInfo(generator = ObjectIdGenerators.PropertyGenerator.class, property = "id"), I use Mixins...

I create an Interface and annotate it: @JsonIdentityInfo(generator = ObjectIdGenerators.PropertyGenerator.class, property = "id") public interface JsonIdReferenceMixIn { }

I create a mixin annotation @Retention(RetentionPolicy.RUNTIME) @Target(ElementType.TYPE) public @interface JsonMixIn { public Class value(); }

then I annotate the class with the mixin annotation instead of using the jackson annotation directly: @JsonMixIn(JsonIdReferenceMixIn.class)

Now I need to add the Mixins to the object mapper. I do: ` SpringClassScanner<JsonMixIn> classScanner = new SpringClassScanner<>(JsonMixIn.class);

    Set<Class<?>> annotatedClasses = classScanner.findAnnotatedClasses("com.myclasspath");

    for (Class annotatedClass: annotatedClasses) {
        JsonMixIn annotation = (JsonMixIn) annotatedClass.getAnnotation(JsonMixIn.class);
        if (annotation.value() != null) {
            objectMapper.addMixIn(annotatedClass, annotation.value());
        }
    }`

siemensoe avatar Sep 23 '20 07:09 siemensoe