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

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

Open b-behan opened this issue 7 years ago • 8 comments

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)

b-behan avatar May 09 '17 04:05 b-behan

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

ticxx avatar Sep 26 '17 12:09 ticxx

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

cowtowncoder avatar Sep 29 '17 02:09 cowtowncoder

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?

bergvandenp avatar Jan 11 '18 21:01 bergvandenp

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

cowtowncoder avatar Jan 12 '18 21:01 cowtowncoder

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 avatar Apr 19 '18 09:04 koscejev

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

koscejev avatar Apr 19 '18 13:04 koscejev

@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 avatar Apr 19 '18 21:04 cowtowncoder

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.

cowtowncoder avatar Apr 24 '18 18:04 cowtowncoder