FasterXML / jackson-databind

General data-binding package for Jackson (2.x): works on streaming API (core) implementation(s)
Apache License 2.0
3.52k stars 1.38k forks source link

[race condition] Failure to deserialize child object when using `@JsonIgnoreProperties` to break cycle #1622

Open b-behan opened 7 years ago

b-behan commented 7 years ago

An issue is being encountered when attempting to deserialize a child object in a parent-child relationship with cyclic references where the @JsonIgnoreProperties annotation is being used to break the cycle. The problem only occurs if there is an attempt to deserialize a child object directly, before making an attempt to deserialize the parent object directly. This occurs in version 2.8.8, but is also reproducible in version 2.9.0.pr3.

The following test code reproduces the issue:

    private static class Parent {
        private String name;
        @JsonIgnoreProperties("parent")
        private List<Child> children;

        public String getName() {
            return name;
        }

        public void setName(String name) {
            this.name = name;
        }

        public List<Child> getChildren() {
            return children;
        }

        public void setChildren(List<Child> children) {
            this.children = children;
        }

        public void addChild(Child child) {
            if (children == null) {
                children = new ArrayList<>();
            }
            children.add(child);
            child.setParent(this);
        }
    }

    private static class Child {
        private Parent parent;
        private String name;

        public Parent getParent() {
            return parent;
        }

        public void setParent(Parent parent) {
            this.parent = parent;
        }

        public String getName() {
            return name;
        }

        public void setName(String name) {
            this.name = name;
        }
    }

    @Test
    public void testSerialization() throws Exception {
        Parent alice = new Parent();
        alice.setName("Alice");
        Child bill = new Child();
        bill.setName("Bill");
        alice.addChild(bill);

        ObjectMapper mapper = new ObjectMapper();
        System.out.println("Jackson version: " + mapper.version());
        String childJson = mapper.writeValueAsString(bill);
        String parentJson = mapper.writeValueAsString(alice);

        // The following line will fail, but will work if the following
        // two lines are reversed.
        mapper.readValue(childJson, Child.class);
        mapper.readValue(parentJson, Parent.class);
    }

This results in the following exception:

com.fasterxml.jackson.databind.JsonMappingException: No _valueDeserializer assigned
 at [Source: {"parent":{"name":"Alice","children":[{"name":"Bill"}]},"name":"Bill"}; line: 1, column: 47] (through reference chain: JsonTest$Child["parent"]->JsonTest$Parent["children"]->java.util.ArrayList[0]->JsonTest$Child["name"])

    at com.fasterxml.jackson.databind.JsonMappingException.from(JsonMappingException.java:270)
    at com.fasterxml.jackson.databind.DeserializationContext.reportMappingException(DeserializationContext.java:1234)
    at com.fasterxml.jackson.databind.deser.impl.FailingDeserializer.deserialize(FailingDeserializer.java:27)
    at com.fasterxml.jackson.databind.deser.SettableBeanProperty.deserialize(SettableBeanProperty.java:504)
    at com.fasterxml.jackson.databind.deser.impl.MethodProperty.deserializeAndSet(MethodProperty.java:104)
    at com.fasterxml.jackson.databind.deser.BeanDeserializer.vanillaDeserialize(BeanDeserializer.java:276)
    at com.fasterxml.jackson.databind.deser.BeanDeserializer.deserialize(BeanDeserializer.java:140)
    at com.fasterxml.jackson.databind.deser.std.CollectionDeserializer.deserialize(CollectionDeserializer.java:287)
    at com.fasterxml.jackson.databind.deser.std.CollectionDeserializer.deserialize(CollectionDeserializer.java:259)
    at com.fasterxml.jackson.databind.deser.std.CollectionDeserializer.deserialize(CollectionDeserializer.java:26)
    at com.fasterxml.jackson.databind.deser.SettableBeanProperty.deserialize(SettableBeanProperty.java:504)
    at com.fasterxml.jackson.databind.deser.impl.MethodProperty.deserializeAndSet(MethodProperty.java:104)
    at com.fasterxml.jackson.databind.deser.BeanDeserializer.vanillaDeserialize(BeanDeserializer.java:276)
    at com.fasterxml.jackson.databind.deser.BeanDeserializer.deserialize(BeanDeserializer.java:140)
    at com.fasterxml.jackson.databind.deser.SettableBeanProperty.deserialize(SettableBeanProperty.java:504)
    at com.fasterxml.jackson.databind.deser.impl.MethodProperty.deserializeAndSet(MethodProperty.java:104)
    at com.fasterxml.jackson.databind.deser.BeanDeserializer.vanillaDeserialize(BeanDeserializer.java:276)
    at com.fasterxml.jackson.databind.deser.BeanDeserializer.deserialize(BeanDeserializer.java:140)
    at com.fasterxml.jackson.databind.ObjectMapper._readMapAndClose(ObjectMapper.java:3798)
    at com.fasterxml.jackson.databind.ObjectMapper.readValue(ObjectMapper.java:2842)
    at JsonTest.testSerialization(JsonTest.java:76)
    at sun.reflect.NativeMethodAccessorImpl.invoke0(Native Method)
    at sun.reflect.NativeMethodAccessorImpl.invoke(NativeMethodAccessorImpl.java:62)
    at sun.reflect.DelegatingMethodAccessorImpl.invoke(DelegatingMethodAccessorImpl.java:43)
    at java.lang.reflect.Method.invoke(Method.java:498)
    at org.junit.runners.model.FrameworkMethod$1.runReflectiveCall(FrameworkMethod.java:50)
    at org.junit.internal.runners.model.ReflectiveCallable.run(ReflectiveCallable.java:12)
    at org.junit.runners.model.FrameworkMethod.invokeExplosively(FrameworkMethod.java:47)
    at org.junit.internal.runners.statements.InvokeMethod.evaluate(InvokeMethod.java:17)
    at org.junit.runners.ParentRunner.runLeaf(ParentRunner.java:325)
    at org.junit.runners.BlockJUnit4ClassRunner.runChild(BlockJUnit4ClassRunner.java:78)
    at org.junit.runners.BlockJUnit4ClassRunner.runChild(BlockJUnit4ClassRunner.java:57)
    at org.junit.runners.ParentRunner$3.run(ParentRunner.java:290)
    at org.junit.runners.ParentRunner$1.schedule(ParentRunner.java:71)
    at org.junit.runners.ParentRunner.runChildren(ParentRunner.java:288)
    at org.junit.runners.ParentRunner.access$000(ParentRunner.java:58)
    at org.junit.runners.ParentRunner$2.evaluate(ParentRunner.java:268)
    at org.junit.runners.ParentRunner.run(ParentRunner.java:363)
    at org.junit.runner.JUnitCore.run(JUnitCore.java:137)
    at com.intellij.junit4.JUnit4IdeaTestRunner.startRunnerWithArgs(JUnit4IdeaTestRunner.java:68)
    at com.intellij.rt.execution.junit.IdeaTestRunner$Repeater.startRunnerWithArgs(IdeaTestRunner.java:51)
    at com.intellij.rt.execution.junit.JUnitStarter.prepareStreamsAndStart(JUnitStarter.java:237)
    at com.intellij.rt.execution.junit.JUnitStarter.main(JUnitStarter.java:70)
ticxx commented 7 years ago

Hi, I have the same behaviour in version 2.8.10 using spring boot. Is this a known bug? Thanks ticxx

cowtowncoder commented 7 years ago

I am surprised that @JsonIgnoreProperties would have any effect here for List-valued property -- I would only expect it to work on POJO types (since Lists do not have properties). It would be nice if it did work (and same for Maps, arrays), but I don't think it does. But that leads to question of why/how exception gets thrown...

bergvandenp commented 6 years ago

It looks like I'm experiencing this issue as well. Usually it works as aspected, but sometimes when we deploy our application, this exception is being thrown. Only a reboot of the jvm "fixes" this issue, so I think some initialisation / caching is loaded using the incorrect order.

@cowtowncoder If I understand correctly, @JsonIgnoreProperties shouldn't be used for this? Maybe @JsonManagedReference would be a better option here?

cowtowncoder commented 6 years ago

@bergvandenp There have been some improvements to thread-safety of class introspection, relatively recently, but I do not remember exact timing. I thought 2.8.10 would have improvements (it's quite recent), but 2.9 did get a cleaned up version. So just in case it was easy enough to test (... which is probably isn't, for rare but nasty bug like this...), I'd try 2.9.3, if you are running on older version.

If problem occurs on pre-2.8.10 version, I'd definitely try 2.8.11 (latest/last 2.8).

koscejev commented 6 years ago

We are experiencing the same issue. Cannot reproduce locally to debug properly, but it occurs quite reliably on our weaker build VMs. Could be some kind of race condition that only happens if the machine is too slow.

Occurs on 2.8.3, 2.8.9, 2.8.11, 2.8.11.1, 2.9.5. Never happens on 2.7.8, 2.7.9.

koscejev commented 6 years ago

Workaround: use allowSetters = true on the @JsonIgnoreProperties annotations.

cowtowncoder commented 6 years ago

@koscejev Thank you for sharing this. Pretty disappointing if it's due to race condition (which sounds plausible based on your description) since quite a bit of work and fixes were made in 2.8 to resolve one specific class of such problems (related to introspection for information accessed via AnnotatedClass); would have guessed it'd be the opposite wrt 2.7.

cowtowncoder commented 6 years ago

FWTW just learned that (as per #1060) use of @JsonIgnoreProperties does indeed work for POJO-valued arrays, Collections. So no mystery there. Still mystery of why there could/would be race condition.