From f20417f91641aaddbf6cf0d79a6a7169a2cfc61b Mon Sep 17 00:00:00 2001 From: Jackson Date: Tue, 9 Dec 2025 19:43:43 +0900 Subject: [PATCH 1/6] Fix #1516: Support @JsonManagedReference on creator properties --- .../deser/BasicDeserializerFactory.java | 7 ++ .../databind/deser/bean/BeanDeserializer.java | 16 +++- .../deser/impl/ManagedReferenceProperty.java | 68 +++++++++++---- .../BackReference1516Test.java | 4 +- .../databind/struct/TestKotlinGithub129.java | 83 +++++++++++++++++++ 5 files changed, 153 insertions(+), 25 deletions(-) rename src/test/java/tools/jackson/databind/{tofix => struct}/BackReference1516Test.java (94%) create mode 100644 src/test/java/tools/jackson/databind/struct/TestKotlinGithub129.java diff --git a/src/main/java/tools/jackson/databind/deser/BasicDeserializerFactory.java b/src/main/java/tools/jackson/databind/deser/BasicDeserializerFactory.java index e7585cd05f..159a7f4247 100644 --- a/src/main/java/tools/jackson/databind/deser/BasicDeserializerFactory.java +++ b/src/main/java/tools/jackson/databind/deser/BasicDeserializerFactory.java @@ -602,6 +602,13 @@ protected SettableBeanProperty constructCreatorProperty(DeserializationContext c SettableBeanProperty prop = CreatorProperty.construct(name, type, property.getWrapperName(), typeDeser, beanDescRef.getClassAnnotations(), param, index, injectable, metadata); + // [databind#1516]: Handle @JsonManagedReference for creator properties + if (intr != null) { + AnnotationIntrospector.ReferenceProperty ref = intr.findReferenceType(config, param); + if (ref != null && ref.isManagedReference()) { + prop.setManagedReferenceName(ref.getName()); + } + } ValueDeserializer deser = findDeserializerFromAnnotation(ctxt, param); if (deser == null) { deser = (ValueDeserializer) type.getValueHandler(); diff --git a/src/main/java/tools/jackson/databind/deser/bean/BeanDeserializer.java b/src/main/java/tools/jackson/databind/deser/bean/BeanDeserializer.java index 81d17bcb75..050eb8b01e 100644 --- a/src/main/java/tools/jackson/databind/deser/bean/BeanDeserializer.java +++ b/src/main/java/tools/jackson/databind/deser/bean/BeanDeserializer.java @@ -10,10 +10,7 @@ import tools.jackson.databind.deser.ReadableObjectId.Referring; import tools.jackson.databind.deser.SettableBeanProperty; import tools.jackson.databind.deser.UnresolvedForwardReference; -import tools.jackson.databind.deser.impl.ExternalTypeHandler; -import tools.jackson.databind.deser.impl.MethodProperty; -import tools.jackson.databind.deser.impl.ObjectIdReader; -import tools.jackson.databind.deser.impl.UnwrappedPropertyHandler; +import tools.jackson.databind.deser.impl.*; import tools.jackson.databind.util.ClassUtil; import tools.jackson.databind.util.IgnorePropertiesUtil; import tools.jackson.databind.util.NameTransformer; @@ -679,6 +676,17 @@ protected Object _deserializeUsingPropertyBased(final JsonParser p, final Deseri } catch (Exception e) { return wrapInstantiationProblem(ctxt, e); } + + // [databind#1516]: Inject back references for managed reference creator properties + for (SettableBeanProperty prop : creator.properties()) { + if (prop instanceof ManagedReferenceProperty managedProp) { + Object value = buffer.getParameter(ctxt, prop); + if (value != null) { + managedProp.injectBackReference(ctxt, bean, value); + } + } + } + p.assignCurrentValue(bean); // [databind#4938] Since 2.19, allow returning `null` from creator, // but if so, need to skip all possibly relevant content diff --git a/src/main/java/tools/jackson/databind/deser/impl/ManagedReferenceProperty.java b/src/main/java/tools/jackson/databind/deser/impl/ManagedReferenceProperty.java index 97d364eda3..6cc8d12627 100644 --- a/src/main/java/tools/jackson/databind/deser/impl/ManagedReferenceProperty.java +++ b/src/main/java/tools/jackson/databind/deser/impl/ManagedReferenceProperty.java @@ -76,31 +76,63 @@ public final void set(DeserializationContext ctxt, Object instance, Object value @Override public Object setAndReturn(DeserializationContext ctxt, Object instance, Object value) { + _setBackReference(ctxt, instance, value); + // and then the forward reference itself + return delegate.setAndReturn(ctxt, instance, value); + } + + /** + * Inject back reference into value without setting forward reference. + * Used for creator properties where forward reference is already set via constructor. + * + * @since 3.0 + */ + public void injectBackReference(DeserializationContext ctxt, Object bean, Object value) + { + _setBackReference(ctxt, bean, value); + } + + /* + /********************************************************************** + /* Helper classes + /********************************************************************** + */ + + /** + * Helper method to inject back reference into value(s). + */ + private void _setBackReference(DeserializationContext ctxt, Object instance, Object value) + { + if (value == null) { + return; + } // 04-Feb-2014, tatu: As per [#390], it may be necessary to switch the // ordering of forward/backward references, and start with back ref. - if (value != null) { - if (_isContainer) { // ok, this gets ugly... but has to do for now - if (value instanceof Object[]) { - for (Object ob : (Object[]) value) { - if (ob != null) { _backProperty.set(ctxt, ob, instance); } + if (_isContainer) { // ok, this gets ugly... but has to do for now + if (value instanceof Object[]) { + for (Object ob : (Object[]) value) { + if (ob != null) { + _backProperty.set(ctxt, ob, instance); } - } else if (value instanceof Collection) { - for (Object ob : (Collection) value) { - if (ob != null) { _backProperty.set(ctxt, ob, instance); } + } + } else if (value instanceof Collection) { + for (Object ob : (Collection) value) { + if (ob != null) { + _backProperty.set(ctxt, ob, instance); } - } else if (value instanceof Map) { - for (Object ob : ((Map) value).values()) { - if (ob != null) { _backProperty.set(ctxt, ob, instance); } + } + } else if (value instanceof Map) { + for (Object ob : ((Map) value).values()) { + if (ob != null) { + _backProperty.set(ctxt, ob, instance); } - } else { - throw new IllegalStateException("Unsupported container type ("+value.getClass().getName() - +") when resolving reference '"+_referenceName+"'"); } } else { - _backProperty.set(ctxt, value, instance); + throw new IllegalStateException("Unsupported container type (" + value.getClass().getName() + + ") when resolving reference '" + _referenceName + "'"); } + } else { + _backProperty.set(ctxt, value, instance); } - // and then the forward reference itself - return delegate.setAndReturn(ctxt, instance, value); - } + } } diff --git a/src/test/java/tools/jackson/databind/tofix/BackReference1516Test.java b/src/test/java/tools/jackson/databind/struct/BackReference1516Test.java similarity index 94% rename from src/test/java/tools/jackson/databind/tofix/BackReference1516Test.java rename to src/test/java/tools/jackson/databind/struct/BackReference1516Test.java index b3add35c8d..cb04ecf400 100644 --- a/src/test/java/tools/jackson/databind/tofix/BackReference1516Test.java +++ b/src/test/java/tools/jackson/databind/struct/BackReference1516Test.java @@ -1,4 +1,4 @@ -package tools.jackson.databind.tofix; +package tools.jackson.databind.struct; import java.beans.ConstructorProperties; @@ -9,7 +9,6 @@ import tools.jackson.databind.ObjectMapper; import tools.jackson.databind.testutil.DatabindTestUtil; -import tools.jackson.databind.testutil.failure.JacksonTestFailureExpected; import static org.junit.jupiter.api.Assertions.assertNotNull; import static org.junit.jupiter.api.Assertions.assertSame; @@ -75,7 +74,6 @@ public ChildObject2(String id, String name, " 'child': { 'id': 'def', 'name':'Bert' }\n" + "}"); - @JacksonTestFailureExpected @Test void withParentCreator() throws Exception { ParentWithCreator result = MAPPER.readValue(PARENT_CHILD_JSON, diff --git a/src/test/java/tools/jackson/databind/struct/TestKotlinGithub129.java b/src/test/java/tools/jackson/databind/struct/TestKotlinGithub129.java new file mode 100644 index 0000000000..3709e95c8f --- /dev/null +++ b/src/test/java/tools/jackson/databind/struct/TestKotlinGithub129.java @@ -0,0 +1,83 @@ +package tools.jackson.databind.struct; + +import java.util.ArrayList; +import java.util.List; + +import org.junit.jupiter.api.Test; + +import com.fasterxml.jackson.annotation.JsonBackReference; +import com.fasterxml.jackson.annotation.JsonCreator; +import com.fasterxml.jackson.annotation.JsonManagedReference; +import com.fasterxml.jackson.annotation.JsonProperty; + +import tools.jackson.databind.ObjectMapper; +import tools.jackson.databind.testutil.DatabindTestUtil; + +import static org.junit.jupiter.api.Assertions.*; + +// [databind#1516] +public class TestKotlinGithub129 extends DatabindTestUtil +{ + private final ObjectMapper MAPPER = newJsonMapper(); + + static class Car { + private final long id; + + @JsonManagedReference + private final List colors; + + @JsonCreator + public Car(@JsonProperty("id") long id, + @JsonProperty("colors") List colors) { + this.id = id; + this.colors = colors != null ? colors : new ArrayList<>(); + } + + public long getId() { return id; } + public List getColors() { return colors; } + } + + static class Color { + private final long id; + private final String code; + + @JsonBackReference + private Car car; + + @JsonCreator + public Color(@JsonProperty("id") long id, + @JsonProperty("code") String code) { + this.id = id; + this.code = code; + } + + public long getId() { return id; } + public String getCode() { return code; } + public Car getCar() { return car; } + public void setCar(Car car) { this.car = car; } + } + + @Test + public void testManagedReferenceOnCreator() throws Exception + { + Car car = new Car(100, new ArrayList<>()); + Color color = new Color(100, "#FFFFF"); + color.setCar(car); + car.getColors().add(color); + + String json = MAPPER.writeValueAsString(car); + Car result = MAPPER.readValue(json, Car.class); + + assertNotNull(result); + assertEquals(100, result.getId()); + assertNotNull(result.getColors()); + assertEquals(1, result.getColors().size()); + + Color resultColor = result.getColors().get(0); + assertEquals(100, resultColor.getId()); + assertEquals("#FFFFF", resultColor.getCode()); + + assertNotNull(resultColor.getCar()); + assertSame(result, resultColor.getCar()); + } +} \ No newline at end of file From de094fd386e94c71b654f3d3a5221210f707fa06 Mon Sep 17 00:00:00 2001 From: Jackson Date: Tue, 9 Dec 2025 20:07:31 +0900 Subject: [PATCH 2/6] Fix #1516: Remove unnecessary injectBackReference method --- .../jackson/databind/deser/bean/BeanDeserializer.java | 2 +- .../databind/deser/impl/ManagedReferenceProperty.java | 11 ----------- 2 files changed, 1 insertion(+), 12 deletions(-) diff --git a/src/main/java/tools/jackson/databind/deser/bean/BeanDeserializer.java b/src/main/java/tools/jackson/databind/deser/bean/BeanDeserializer.java index 050eb8b01e..a08e988980 100644 --- a/src/main/java/tools/jackson/databind/deser/bean/BeanDeserializer.java +++ b/src/main/java/tools/jackson/databind/deser/bean/BeanDeserializer.java @@ -682,7 +682,7 @@ protected Object _deserializeUsingPropertyBased(final JsonParser p, final Deseri if (prop instanceof ManagedReferenceProperty managedProp) { Object value = buffer.getParameter(ctxt, prop); if (value != null) { - managedProp.injectBackReference(ctxt, bean, value); + managedProp.set(ctxt, bean, value); } } } diff --git a/src/main/java/tools/jackson/databind/deser/impl/ManagedReferenceProperty.java b/src/main/java/tools/jackson/databind/deser/impl/ManagedReferenceProperty.java index 6cc8d12627..e314494be3 100644 --- a/src/main/java/tools/jackson/databind/deser/impl/ManagedReferenceProperty.java +++ b/src/main/java/tools/jackson/databind/deser/impl/ManagedReferenceProperty.java @@ -81,17 +81,6 @@ public Object setAndReturn(DeserializationContext ctxt, Object instance, Object return delegate.setAndReturn(ctxt, instance, value); } - /** - * Inject back reference into value without setting forward reference. - * Used for creator properties where forward reference is already set via constructor. - * - * @since 3.0 - */ - public void injectBackReference(DeserializationContext ctxt, Object bean, Object value) - { - _setBackReference(ctxt, bean, value); - } - /* /********************************************************************** /* Helper classes From c1b9dc19982eca583a611a2f009a701728c020c2 Mon Sep 17 00:00:00 2001 From: Jackson Date: Sun, 14 Dec 2025 13:01:00 +0900 Subject: [PATCH 3/6] Rename #1516: TestKotlinGithub129 to KotlinGithub129Test --- .../{TestKotlinGithub129.java => KotlinGithub129Test.java} | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) rename src/test/java/tools/jackson/databind/struct/{TestKotlinGithub129.java => KotlinGithub129Test.java} (97%) diff --git a/src/test/java/tools/jackson/databind/struct/TestKotlinGithub129.java b/src/test/java/tools/jackson/databind/struct/KotlinGithub129Test.java similarity index 97% rename from src/test/java/tools/jackson/databind/struct/TestKotlinGithub129.java rename to src/test/java/tools/jackson/databind/struct/KotlinGithub129Test.java index 3709e95c8f..3d8ede0c1b 100644 --- a/src/test/java/tools/jackson/databind/struct/TestKotlinGithub129.java +++ b/src/test/java/tools/jackson/databind/struct/KotlinGithub129Test.java @@ -16,7 +16,7 @@ import static org.junit.jupiter.api.Assertions.*; // [databind#1516] -public class TestKotlinGithub129 extends DatabindTestUtil +public class KotlinGithub129Test extends DatabindTestUtil { private final ObjectMapper MAPPER = newJsonMapper(); From cab6d1644511d91a085c48504d8970db0f0c96c7 Mon Sep 17 00:00:00 2001 From: Jackson Date: Sun, 14 Dec 2025 13:17:17 +0900 Subject: [PATCH 4/6] Fix #1516: Optimize managed reference detection --- .../databind/deser/bean/BeanDeserializer.java | 12 +++++---- .../deser/bean/PropertyBasedCreator.java | 25 +++++++++++++++++++ 2 files changed, 32 insertions(+), 5 deletions(-) diff --git a/src/main/java/tools/jackson/databind/deser/bean/BeanDeserializer.java b/src/main/java/tools/jackson/databind/deser/bean/BeanDeserializer.java index a08e988980..695d91b35c 100644 --- a/src/main/java/tools/jackson/databind/deser/bean/BeanDeserializer.java +++ b/src/main/java/tools/jackson/databind/deser/bean/BeanDeserializer.java @@ -678,11 +678,13 @@ protected Object _deserializeUsingPropertyBased(final JsonParser p, final Deseri } // [databind#1516]: Inject back references for managed reference creator properties - for (SettableBeanProperty prop : creator.properties()) { - if (prop instanceof ManagedReferenceProperty managedProp) { - Object value = buffer.getParameter(ctxt, prop); - if (value != null) { - managedProp.set(ctxt, bean, value); + if (creator.hasManagedReferenceProperties()) { + for (SettableBeanProperty prop : creator.properties()) { + if (prop instanceof ManagedReferenceProperty managedProp) { + Object value = buffer.getParameter(ctxt, prop); + if (value != null) { + managedProp.set(ctxt, bean, value); + } } } } diff --git a/src/main/java/tools/jackson/databind/deser/bean/PropertyBasedCreator.java b/src/main/java/tools/jackson/databind/deser/bean/PropertyBasedCreator.java index f94f7fdd68..301b58399b 100644 --- a/src/main/java/tools/jackson/databind/deser/bean/PropertyBasedCreator.java +++ b/src/main/java/tools/jackson/databind/deser/bean/PropertyBasedCreator.java @@ -9,6 +9,7 @@ import tools.jackson.databind.deser.SettableAnyProperty; import tools.jackson.databind.deser.SettableBeanProperty; import tools.jackson.databind.deser.ValueInstantiator; +import tools.jackson.databind.deser.impl.ManagedReferenceProperty; import tools.jackson.databind.deser.impl.ObjectIdReader; import tools.jackson.databind.util.NameTransformer; @@ -54,6 +55,14 @@ public final class PropertyBasedCreator */ protected final BitSet _injectablePropIndexes; + /** + * Flag indicating whether any creator properties are ManagedReferenceProperty instances, + * used to optimize deserialization by skipping back-reference injection when not needed. + * + * @since 3.1 + */ + protected final boolean _hasManagedReferenceProperties; + /* /********************************************************************** /* Construction, initialization @@ -93,6 +102,7 @@ protected PropertyBasedCreator(DeserializationContext ctxt, _propertyCount = len; _propertiesInOrder = new SettableBeanProperty[len]; BitSet injectablePropIndexes = null; + boolean hasManagedRef = false; for (int i = 0; i < len; ++i) { SettableBeanProperty prop = creatorProps[i]; @@ -107,9 +117,16 @@ protected PropertyBasedCreator(DeserializationContext ctxt, } injectablePropIndexes.set(i); } + + // [databind#1516]: detect whether any ManagedReferenceProperty exists + // so we can avoid iterating over all properties when none are present + if (!hasManagedRef && (prop instanceof ManagedReferenceProperty)) { + hasManagedRef = true; + } } _injectablePropIndexes = injectablePropIndexes; + _hasManagedReferenceProperties = hasManagedRef; } protected PropertyBasedCreator(PropertyBasedCreator base, @@ -121,6 +138,7 @@ protected PropertyBasedCreator(PropertyBasedCreator base, _injectablePropIndexes = base._injectablePropIndexes; _propertyLookup = propertyLookup; _propertiesInOrder = allProperties; + _hasManagedReferenceProperties = base._hasManagedReferenceProperties; } /** @@ -238,6 +256,13 @@ public SettableBeanProperty findCreatorProperty(int propertyIndex) { return null; } + /** + * @since 3.1 + */ + public boolean hasManagedReferenceProperties() { + return _hasManagedReferenceProperties; + } + /* /********************************************************************** /* Building process From 52de91fe592a7530582dc6cf26746124c21f956a Mon Sep 17 00:00:00 2001 From: Tatu Saloranta Date: Tue, 16 Dec 2025 20:33:20 -0800 Subject: [PATCH 5/6] Add release notes, rename test class, streamline --- release-notes/CREDITS | 3 +++ release-notes/VERSION | 3 +++ .../jackson/databind/deser/bean/PropertyBasedCreator.java | 4 +--- ...nGithub129Test.java => Kotlin129ManagedReferenceTest.java} | 4 ++-- 4 files changed, 9 insertions(+), 5 deletions(-) rename src/test/java/tools/jackson/databind/struct/{KotlinGithub129Test.java => Kotlin129ManagedReferenceTest.java} (93%) diff --git a/release-notes/CREDITS b/release-notes/CREDITS index 8b6740471a..cf93689f77 100644 --- a/release-notes/CREDITS +++ b/release-notes/CREDITS @@ -124,6 +124,9 @@ Oliver Drotbohm (@odrotbohm) [3.1.0] @JacksonJang + * Contributed fix for #1516: Problem with multi-argument Creator with + `@JsonBackReference` property + [3.1.0] * Contributed fix for #4629: `@JsonIncludeProperties` and `@JsonIgnoreProperties` ignored when deserializing Records [3.1.0] diff --git a/release-notes/VERSION b/release-notes/VERSION index fd4597f0c5..ac39ecd9cb 100644 --- a/release-notes/VERSION +++ b/release-notes/VERSION @@ -11,6 +11,9 @@ Versions: 3.x (for earlier see VERSION-2.x) #1196: Add opt-in error collection for deserialization (requested by @odrotbohm) (contributed by @sri-adarsh-kumar) +#1516: Problem with multi-argument Creator with `@JsonBackReference` property + (reported by @sarahlikesglitter) + (fix by @JacksonJang) #1654: @JsonDeserialize(contentUsing=...) is ignored if content type is determined by @JsonTypeInfo (reported by @pdegoeje) diff --git a/src/main/java/tools/jackson/databind/deser/bean/PropertyBasedCreator.java b/src/main/java/tools/jackson/databind/deser/bean/PropertyBasedCreator.java index 301b58399b..077ad61909 100644 --- a/src/main/java/tools/jackson/databind/deser/bean/PropertyBasedCreator.java +++ b/src/main/java/tools/jackson/databind/deser/bean/PropertyBasedCreator.java @@ -120,9 +120,7 @@ protected PropertyBasedCreator(DeserializationContext ctxt, // [databind#1516]: detect whether any ManagedReferenceProperty exists // so we can avoid iterating over all properties when none are present - if (!hasManagedRef && (prop instanceof ManagedReferenceProperty)) { - hasManagedRef = true; - } + hasManagedRef |= prop instanceof ManagedReferenceProperty; } _injectablePropIndexes = injectablePropIndexes; diff --git a/src/test/java/tools/jackson/databind/struct/KotlinGithub129Test.java b/src/test/java/tools/jackson/databind/struct/Kotlin129ManagedReferenceTest.java similarity index 93% rename from src/test/java/tools/jackson/databind/struct/KotlinGithub129Test.java rename to src/test/java/tools/jackson/databind/struct/Kotlin129ManagedReferenceTest.java index 3d8ede0c1b..985922f19d 100644 --- a/src/test/java/tools/jackson/databind/struct/KotlinGithub129Test.java +++ b/src/test/java/tools/jackson/databind/struct/Kotlin129ManagedReferenceTest.java @@ -15,8 +15,8 @@ import static org.junit.jupiter.api.Assertions.*; -// [databind#1516] -public class KotlinGithub129Test extends DatabindTestUtil +// For [databind#1516], https://github.com/FasterXML/jackson-module-kotlin/issues/129 +public class Kotlin129ManagedReferenceTest extends DatabindTestUtil { private final ObjectMapper MAPPER = newJsonMapper(); From 9bf78ea0bf520c51ae3a45cead6a21e238d06327 Mon Sep 17 00:00:00 2001 From: Tatu Saloranta Date: Tue, 16 Dec 2025 20:41:12 -0800 Subject: [PATCH 6/6] Fix tiny typo --- .../jackson/databind/deser/impl/ManagedReferenceProperty.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/main/java/tools/jackson/databind/deser/impl/ManagedReferenceProperty.java b/src/main/java/tools/jackson/databind/deser/impl/ManagedReferenceProperty.java index e314494be3..9ccb3faa54 100644 --- a/src/main/java/tools/jackson/databind/deser/impl/ManagedReferenceProperty.java +++ b/src/main/java/tools/jackson/databind/deser/impl/ManagedReferenceProperty.java @@ -83,7 +83,7 @@ public Object setAndReturn(DeserializationContext ctxt, Object instance, Object /* /********************************************************************** - /* Helper classes + /* Helper methods /********************************************************************** */