From 0c6797d507e77368e31e9f8129979edb0061850f Mon Sep 17 00:00:00 2001 From: Tatu Saloranta Date: Tue, 12 Nov 2024 17:41:56 -0800 Subject: [PATCH 1/7] Try to merge #4271 (for #1467) to `master`; problem with lack of access to DeserializationContext --- release-notes/CREDITS-2.x | 4 + release-notes/VERSION-2.x | 2 + .../deser/BasicDeserializerFactory.java | 27 +-- .../databind/deser/SettableBeanProperty.java | 22 +++ .../databind/deser/bean/BeanDeserializer.java | 5 + .../deser/bean/BeanDeserializerBase.java | 35 ++-- .../deser/bean/PropertyBasedCreator.java | 55 ++++++ .../deser/impl/UnwrappedPropertyHandler.java | 90 ++++++--- .../introspect/POJOPropertiesCollector.java | 18 +- .../records/RecordWithJsonUnwrappedTest.java | 39 ++++ ...pedPropertyBasedCreatorWithPrefixTest.java | 41 +++++ .../struct/UnwrappedWithCreatorTest.java | 171 ++++++++++++++++++ 12 files changed, 447 insertions(+), 62 deletions(-) create mode 100644 src/test-jdk17/java/tools/jackson/databind/records/RecordWithJsonUnwrappedTest.java create mode 100644 src/test/java/tools/jackson/databind/struct/UnwrappedPropertyBasedCreatorWithPrefixTest.java create mode 100644 src/test/java/tools/jackson/databind/struct/UnwrappedWithCreatorTest.java diff --git a/release-notes/CREDITS-2.x b/release-notes/CREDITS-2.x index 2535cd7f82..ca94908aa9 100644 --- a/release-notes/CREDITS-2.x +++ b/release-notes/CREDITS-2.x @@ -1853,6 +1853,10 @@ Mike Minicki (@martel) * Reported #4788: `EnumFeature.WRITE_ENUMS_TO_LOWERCASE` overrides `@JsonProperty` values (2.18.2) +Liam Feid (@fxshlein) + * Contributed #1467: Support `@JsonUnwrapped` with `@JsonCreator` + (2.19.0) + @SandeepGaur2016 * Contributed fix for #2461: Nested `@JsonUnwrapped` property names not correctly handled (2.19.0) diff --git a/release-notes/VERSION-2.x b/release-notes/VERSION-2.x index 45fa98894c..8e9cf15ccd 100644 --- a/release-notes/VERSION-2.x +++ b/release-notes/VERSION-2.x @@ -6,6 +6,8 @@ Project: jackson-databind 2.19.0 (not yet released) +#1467: Support `@JsonUnwrapped` with `@JsonCreator` + (implementation by Liam F) #2461: Nested `@JsonUnwrapped` property names not correctly handled (reported by @plovell) (fix contributed by @SandeepGaur2016) diff --git a/src/main/java/tools/jackson/databind/deser/BasicDeserializerFactory.java b/src/main/java/tools/jackson/databind/deser/BasicDeserializerFactory.java index 8001c65254..a5044ed679 100644 --- a/src/main/java/tools/jackson/databind/deser/BasicDeserializerFactory.java +++ b/src/main/java/tools/jackson/databind/deser/BasicDeserializerFactory.java @@ -13,6 +13,7 @@ import tools.jackson.databind.cfg.*; import tools.jackson.databind.deser.bean.CreatorCandidate; import tools.jackson.databind.deser.bean.CreatorCollector; +import tools.jackson.databind.deser.impl.UnwrappedPropertyHandler; import tools.jackson.databind.deser.jackson.JsonNodeDeserializer; import tools.jackson.databind.deser.jackson.TokenBufferDeserializer; import tools.jackson.databind.deser.jdk.*; @@ -48,12 +49,6 @@ public abstract class BasicDeserializerFactory private final static Class CLASS_MAP_ENTRY = Map.Entry.class; private final static Class CLASS_SERIALIZABLE = Serializable.class; - /** - * We need a placeholder for creator properties that don't have name - * but are marked with `@JsonWrapped` annotation. - */ - protected final static PropertyName UNWRAPPED_CREATOR_PARAM_NAME = new PropertyName("@JsonUnwrapped"); - /* /********************************************************************** /* Config @@ -363,11 +358,8 @@ private void _addImplicitDelegatingConstructors(DeserializationContext ctxt, } NameTransformer unwrapper = intr.findUnwrappingNameTransformer(config, param); if (unwrapper != null) { - _reportUnwrappedCreatorProperty(ctxt, beanDesc, param); - /* - properties[i] = constructCreatorProperty(ctxt, beanDesc, UNWRAPPED_CREATOR_PARAM_NAME, i, param, null); - ++explicitNameCount; - */ + properties[i] = constructCreatorProperty(ctxt, beanDesc, + UnwrappedPropertyHandler.creatorParamName(i), i, param, null); } } @@ -489,7 +481,8 @@ private void _addSelectedPropertiesBasedCreator(DeserializationContext ctxt, // as that will not work with Creators well at all NameTransformer unwrapper = intr.findUnwrappingNameTransformer(config, param); if (unwrapper != null) { - _reportUnwrappedCreatorProperty(ctxt, beanDesc, param); + properties[i] = constructCreatorProperty(ctxt, beanDesc, + UnwrappedPropertyHandler.creatorParamName(i), i, param, null); } // Must be injectable or have name; without either won't work if ((name == null) && (injectId == null)) { @@ -556,16 +549,6 @@ private boolean _handleSingleArgumentCreator(CreatorCollector creators, return false; } - // 01-Dec-2016, tatu: As per [databind#265] we cannot yet support passing - // of unwrapped values through creator properties, so fail fast - private void _reportUnwrappedCreatorProperty(DeserializationContext ctxt, - BeanDescription beanDesc, AnnotatedParameter param) - { - ctxt.reportBadTypeDefinition(beanDesc, -"Cannot define Creator parameter %d as `@JsonUnwrapped`: combination not yet supported", - param.getIndex()); - } - /** * Method that will construct a property object that represents * a logical property passed via Creator (constructor or static diff --git a/src/main/java/tools/jackson/databind/deser/SettableBeanProperty.java b/src/main/java/tools/jackson/databind/deser/SettableBeanProperty.java index a99f62b806..e017a73154 100644 --- a/src/main/java/tools/jackson/databind/deser/SettableBeanProperty.java +++ b/src/main/java/tools/jackson/databind/deser/SettableBeanProperty.java @@ -12,6 +12,7 @@ import tools.jackson.databind.jsontype.TypeDeserializer; import tools.jackson.databind.util.Annotations; import tools.jackson.databind.util.ClassUtil; +import tools.jackson.databind.util.NameTransformer; import tools.jackson.databind.util.ViewMatcher; /** @@ -577,6 +578,27 @@ public final Object deserializeWith(JsonParser p, DeserializationContext ctxt, return value; } + /** + * Returns a copy of this property, unwrapped using given {@link NameTransformer}. + * + * @since 2.19 + */ + public SettableBeanProperty unwrapped(DeserializationContext ctxt, NameTransformer xf) + { + String newName = xf.transform(getName()); + SettableBeanProperty renamed = withSimpleName(newName); + ValueDeserializer deser = renamed.getValueDeserializer(); + if (deser != null) { + @SuppressWarnings("unchecked") + ValueDeserializer newDeser = (ValueDeserializer) + deser.unwrappingDeserializer(ctxt, xf); + if (newDeser != deser) { + renamed = renamed.withValueDeserializer(newDeser); + } + } + return renamed; + } + /* /********************************************************************** /* Helper methods 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 f4f237d1f5..6741aa5a1d 100644 --- a/src/main/java/tools/jackson/databind/deser/bean/BeanDeserializer.java +++ b/src/main/java/tools/jackson/databind/deser/bean/BeanDeserializer.java @@ -1101,6 +1101,11 @@ protected Object deserializeUsingPropertyBasedWithUnwrapped(JsonParser p, Deseri } } + // We could still have some not-yet-set creator properties that are unwrapped. + // These have to be processed last, because 'tokens' contains all properties + // that remain after regular deserialization. + buffer = _unwrappedPropertyHandler.processUnwrappedCreatorProperties(p, ctxt, buffer, tokens); + // We hit END_OBJECT, so: Object bean; try { diff --git a/src/main/java/tools/jackson/databind/deser/bean/BeanDeserializerBase.java b/src/main/java/tools/jackson/databind/deser/bean/BeanDeserializerBase.java index b954bb978e..5e6629a78d 100644 --- a/src/main/java/tools/jackson/databind/deser/bean/BeanDeserializerBase.java +++ b/src/main/java/tools/jackson/databind/deser/bean/BeanDeserializerBase.java @@ -308,7 +308,6 @@ protected BeanDeserializerBase(BeanDeserializerBase src, _valueInstantiator = src._valueInstantiator; _delegateDeserializer = src._delegateDeserializer; _arrayDelegateDeserializer = src._arrayDelegateDeserializer; - _propertyBasedCreator = src._propertyBasedCreator; _backRefs = src._backRefs; _ignorableProps = src._ignorableProps; @@ -319,9 +318,24 @@ protected BeanDeserializerBase(BeanDeserializerBase src, _objectIdReader = src._objectIdReader; _nonStandardCreation = src._nonStandardCreation; + UnwrappedPropertyHandler uph = src._unwrappedPropertyHandler; + PropertyBasedCreator pbc = src._propertyBasedCreator; - _unwrappedPropertyHandler = unwrapHandler; - _beanProperties = renamedProperties; + if (unwrapHandler != null) { + // delegate further unwraps, if any + if (uph != null) { // got handler, delegate + uph = uph.renameAll(unwrapHandler); + } + // and handle direct unwrapping as well: + if (pbc != null) { + pbc = pbc.renameAll(unwrapHandler); + } + _beanProperties = src._beanProperties.renameAll(unwrapHandler); + } else { + _beanProperties = src._beanProperties; + } + _propertyBasedCreator = pbc; + _unwrappedPropertyHandler = uph; _needViewProcesing = src._needViewProcesing; _serializationShape = src._serializationShape; @@ -550,7 +564,13 @@ public void resolve(DeserializationContext ctxt) if (unwrapped == null) { unwrapped = new UnwrappedPropertyHandler(); } - unwrapped.addProperty(prop); + + if (prop instanceof CreatorProperty) { + unwrapped.addCreatorProperty(prop); + } else { + unwrapped.addProperty(prop); + } + // 12-Dec-2014, tatu: As per [databind#647], we will have problems if // the original property is left in place. So let's remove it now. // 25-Mar-2017, tatu: Wonder if this could be problematic wrt creators? @@ -973,13 +993,6 @@ protected NameTransformer _findPropertyUnwrapper(DeserializationContext ctxt, NameTransformer unwrapper = ctxt.getAnnotationIntrospector().findUnwrappingNameTransformer( ctxt.getConfig(), am); if (unwrapper != null) { - // 01-Dec-2016, tatu: As per [databind#265] we cannot yet support passing - // of unwrapped values through creator properties, so fail fast - if (prop instanceof CreatorProperty) { - ctxt.reportBadDefinition(getValueType(), String.format( - "Cannot define Creator property \"%s\" as `@JsonUnwrapped`: combination not yet supported", - prop.getName())); - } return unwrapper; } } 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 40d5bec8a0..52b4b74dc0 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.SettableBeanProperty; import tools.jackson.databind.deser.ValueInstantiator; import tools.jackson.databind.deser.impl.ObjectIdReader; +import tools.jackson.databind.util.NameTransformer; /** * Object that is used to collect arguments for non-default creator @@ -92,6 +93,19 @@ protected PropertyBasedCreator(DeserializationContext ctxt, } } + /** + * @since 2.19 + */ + protected PropertyBasedCreator(PropertyBasedCreator base, + HashMap propertyLookup, + SettableBeanProperty[] allProperties) + { + _propertyCount = base._propertyCount; + _valueInstantiator = base._valueInstantiator; + _propertyLookup = propertyLookup; + _propertiesInOrder = allProperties; + } + /** * Factory method used for building actual instances to be used with POJOS: * resolves deserializers, checks for "null values". @@ -143,6 +157,47 @@ public static PropertyBasedCreator construct(DeserializationContext ctxt, caseInsensitive, false); } + /** + * Mutant factory method for constructing a map where the names of all properties + * are transformed using the given {@link NameTransformer}. + * + * @since 2.19 + */ + public PropertyBasedCreator renameAll(DeserializationContext ctxt, + NameTransformer transformer) + { + if (transformer == null || (transformer == NameTransformer.NOP)) { + return this; + } + + final int len = _propertiesInOrder.length; + HashMap newLookup = new HashMap<>(_propertyLookup); + List newProps = new ArrayList<>(len); + + for (SettableBeanProperty prop : _propertiesInOrder) { + if (prop == null) { + newProps.add(null); + continue; + } + + SettableBeanProperty renamedProperty = prop.unwrapped(ctxt, transformer); + String oldName = prop.getName(); + String newName = renamedProperty.getName(); + + newProps.add(renamedProperty); + + if (!oldName.equals(newName) && newLookup.containsKey(oldName)) { + newLookup.remove(oldName); + newLookup.put(newName, renamedProperty); + } + } + + return new PropertyBasedCreator(this, + newLookup, + newProps.toArray(new SettableBeanProperty[0]) + ); + } + /* /********************************************************************** /* Accessors diff --git a/src/main/java/tools/jackson/databind/deser/impl/UnwrappedPropertyHandler.java b/src/main/java/tools/jackson/databind/deser/impl/UnwrappedPropertyHandler.java index 574f111060..9d21216369 100644 --- a/src/main/java/tools/jackson/databind/deser/impl/UnwrappedPropertyHandler.java +++ b/src/main/java/tools/jackson/databind/deser/impl/UnwrappedPropertyHandler.java @@ -3,10 +3,10 @@ import java.util.*; import tools.jackson.core.*; -import tools.jackson.core.util.InternCache; import tools.jackson.databind.DeserializationContext; -import tools.jackson.databind.ValueDeserializer; +import tools.jackson.databind.PropertyName; import tools.jackson.databind.deser.SettableBeanProperty; +import tools.jackson.databind.deser.bean.PropertyValueBuffer; import tools.jackson.databind.util.NameTransformer; import tools.jackson.databind.util.TokenBuffer; @@ -17,16 +17,35 @@ */ public class UnwrappedPropertyHandler { + /** + * @since 2.19 + */ + public static final String JSON_UNWRAPPED_NAME_PREFIX = "@JsonUnwrapped/"; + + /** + * @since 2.19 + */ + protected final List _creatorProperties; protected final List _properties; public UnwrappedPropertyHandler() { - _properties = new ArrayList(); + _creatorProperties = new ArrayList<>(); + _properties = new ArrayList<>(); } - protected UnwrappedPropertyHandler(List props) { + protected UnwrappedPropertyHandler(List creatorProps, + List props) { + _creatorProperties = creatorProps; _properties = props; } + /** + * @since 2.19 + */ + public void addCreatorProperty(SettableBeanProperty property) { + _creatorProperties.add(property); + } + public void addProperty(SettableBeanProperty property) { _properties.add(property); } @@ -34,41 +53,60 @@ public void addProperty(SettableBeanProperty property) { public UnwrappedPropertyHandler renameAll(DeserializationContext ctxt, NameTransformer transformer) { - ArrayList newProps = new ArrayList(_properties.size()); - for (SettableBeanProperty prop : _properties) { - String newName = transformer.transform(prop.getName()); - newName = InternCache.instance.intern(newName); - prop = prop.withSimpleName(newName); - ValueDeserializer deser = prop.getValueDeserializer(); - if (deser != null) { - @SuppressWarnings("unchecked") - ValueDeserializer newDeser = (ValueDeserializer) - deser.unwrappingDeserializer(ctxt, transformer); - if (newDeser != deser) { - prop = prop.withValueDeserializer(newDeser); - } + return new UnwrappedPropertyHandler( + renameProperties(ctxt,_creatorProperties, transformer), + renameProperties(ctxt, _properties, transformer) + ); + } + + private List renameProperties(DeserializationContext ctxt, + Collection properties, + NameTransformer transformer + ) { + List newProps = new ArrayList<>(properties.size()); + for (SettableBeanProperty prop : properties) { + if (prop == null) { + newProps.add(null); + continue; } - newProps.add(prop); + + newProps.add(prop.unwrapped(ctxt, transformer)); } - return new UnwrappedPropertyHandler(newProps); + return newProps; } - /* - public List getHandledProperties() { - return Collections.unmodifiableList(_properties); + /** + * @since 2.19 + */ + public PropertyValueBuffer processUnwrappedCreatorProperties(JsonParser originalParser, + DeserializationContext ctxt, PropertyValueBuffer values, TokenBuffer buffered) + { + for (SettableBeanProperty prop : _creatorProperties) { + JsonParser p = buffered.asParserOnFirstToken(ctxt); + values.assignParameter(prop, prop.deserialize(p, ctxt)); + } + + return values; } - */ @SuppressWarnings("resource") public Object processUnwrapped(JsonParser originalParser, DeserializationContext ctxt, Object bean, TokenBuffer buffered) - throws JacksonException { - for (int i = 0, len = _properties.size(); i < len; ++i) { - SettableBeanProperty prop = _properties.get(i); + for (SettableBeanProperty prop : _properties) { JsonParser p = buffered.asParserOnFirstToken(ctxt); prop.deserializeAndSet(p, ctxt, bean); } return bean; } + + /** + * Generates a placeholder name for creator properties that don't have a name, + * but are marked with `@JsonWrapped` annotation. + * + * @since 2.19 + */ + public static PropertyName creatorParamName(int index) { + return new PropertyName(JSON_UNWRAPPED_NAME_PREFIX + index); + } } diff --git a/src/main/java/tools/jackson/databind/introspect/POJOPropertiesCollector.java b/src/main/java/tools/jackson/databind/introspect/POJOPropertiesCollector.java index 4fc22cfa05..cb4ee5bcb7 100644 --- a/src/main/java/tools/jackson/databind/introspect/POJOPropertiesCollector.java +++ b/src/main/java/tools/jackson/databind/introspect/POJOPropertiesCollector.java @@ -11,6 +11,7 @@ import tools.jackson.databind.cfg.ConstructorDetector; import tools.jackson.databind.cfg.HandlerInstantiator; import tools.jackson.databind.cfg.MapperConfig; +import tools.jackson.databind.deser.impl.UnwrappedPropertyHandler; import tools.jackson.databind.jdk14.JDK14Util; import tools.jackson.databind.util.ClassUtil; @@ -982,10 +983,21 @@ private void _addCreatorParams(Map props, final boolean hasExplicit = (explName != null); final POJOPropertyBuilder prop; + // neither implicit nor explicit name? if (!hasExplicit && (implName == null)) { - // Important: if neither implicit nor explicit name, cannot make use of - // this creator parameter -- may or may not be a problem, verified at a later point. - prop = null; + boolean isUnwrapping = _annotationIntrospector.findUnwrappingNameTransformer(_config, param) != null; + + if (isUnwrapping) { + // If unwrapping, can use regardless of name; we will use a placeholder name + // anyway to try to avoid name conflicts. + PropertyName name = UnwrappedPropertyHandler.creatorParamName(param.getIndex()); + prop = _property(props, name); + prop.addCtor(param, name, false, true, false); + } else { + // Without name, cannot make use of this creator parameter -- may or may not + // be a problem, verified at a later point. + prop = null; + } } else { // 27-Dec-2019, tatu: [databind#2527] may need to rename according to field if (implName != null) { diff --git a/src/test-jdk17/java/tools/jackson/databind/records/RecordWithJsonUnwrappedTest.java b/src/test-jdk17/java/tools/jackson/databind/records/RecordWithJsonUnwrappedTest.java new file mode 100644 index 0000000000..93bf33f4a2 --- /dev/null +++ b/src/test-jdk17/java/tools/jackson/databind/records/RecordWithJsonUnwrappedTest.java @@ -0,0 +1,39 @@ +package tools.jackson.databind.records; + +import org.junit.jupiter.api.Test; + +import com.fasterxml.jackson.annotation.JsonUnwrapped; +import tools.jackson.databind.ObjectMapper; +import tools.jackson.databind.node.ObjectNode; +import tools.jackson.databind.testutil.DatabindTestUtil; + +import static org.junit.jupiter.api.Assertions.*; + +public class RecordWithJsonUnwrappedTest extends DatabindTestUtil +{ + record RecordWithJsonUnwrapped(String unrelated, @JsonUnwrapped Inner inner) { + } + + record Inner(String property1, String property2) { + } + + private final ObjectMapper MAPPER = newJsonMapper(); + + @Test + public void testUnwrappedWithRecord() throws Exception + { + RecordWithJsonUnwrapped initial = new RecordWithJsonUnwrapped("unrelatedValue", new Inner("value1", "value2")); + + ObjectNode tree = MAPPER.valueToTree(initial); + + assertEquals("unrelatedValue", tree.get("unrelated").textValue()); + assertEquals("value1", tree.get("property1").textValue()); + assertEquals("value2", tree.get("property2").textValue()); + + RecordWithJsonUnwrapped outer = MAPPER.treeToValue(tree, RecordWithJsonUnwrapped.class); + + assertEquals("unrelatedValue", outer.unrelated()); + assertEquals("value1", outer.inner().property1()); + assertEquals("value2", outer.inner().property2()); + } +} diff --git a/src/test/java/tools/jackson/databind/struct/UnwrappedPropertyBasedCreatorWithPrefixTest.java b/src/test/java/tools/jackson/databind/struct/UnwrappedPropertyBasedCreatorWithPrefixTest.java new file mode 100644 index 0000000000..c685047b02 --- /dev/null +++ b/src/test/java/tools/jackson/databind/struct/UnwrappedPropertyBasedCreatorWithPrefixTest.java @@ -0,0 +1,41 @@ +package tools.jackson.databind.struct; + +import org.junit.jupiter.api.Test; + +import com.fasterxml.jackson.annotation.JsonProperty; +import com.fasterxml.jackson.annotation.JsonUnwrapped; +import tools.jackson.databind.ObjectMapper; +import tools.jackson.databind.testutil.DatabindTestUtil; + +import static org.junit.jupiter.api.Assertions.*; + +public class UnwrappedPropertyBasedCreatorWithPrefixTest extends DatabindTestUtil +{ + static class Outer { + @JsonUnwrapped(prefix = "inner-") + Inner inner; + } + + static class Inner { + private final String _property; + + public Inner(@JsonProperty("property") String property) { + _property = property; + } + + public String getProperty() { + return _property; + } + } + + private final ObjectMapper MAPPER = newJsonMapper(); + + @Test + public void testUnwrappedWithJsonCreatorWithExplicitWithoutName() throws Exception + { + String json = "{\"inner-property\": \"value\"}"; + Outer outer = MAPPER.readValue(json, Outer.class); + + assertEquals("value", outer.inner.getProperty()); + } +} diff --git a/src/test/java/tools/jackson/databind/struct/UnwrappedWithCreatorTest.java b/src/test/java/tools/jackson/databind/struct/UnwrappedWithCreatorTest.java new file mode 100644 index 0000000000..d733c58b30 --- /dev/null +++ b/src/test/java/tools/jackson/databind/struct/UnwrappedWithCreatorTest.java @@ -0,0 +1,171 @@ +package tools.jackson.databind.struct; + +import org.junit.jupiter.api.Test; + +import com.fasterxml.jackson.annotation.*; + +import tools.jackson.databind.*; +import tools.jackson.databind.testutil.DatabindTestUtil; + +import static org.junit.jupiter.api.Assertions.*; + +/** + * Tests to verify [databind#1467]. + */ +public class UnwrappedWithCreatorTest extends DatabindTestUtil +{ + static class ExplicitWithoutName { + private final String _unrelated; + private final Inner _inner; + + @JsonCreator + public ExplicitWithoutName(@JsonProperty("unrelated") String unrelated, @JsonUnwrapped Inner inner) { + _unrelated = unrelated; + _inner = inner; + } + + public String getUnrelated() { + return _unrelated; + } + + @JsonUnwrapped + public Inner getInner() { + return _inner; + } + } + + static class ExplicitWithName { + private final String _unrelated; + private final Inner _inner; + + @JsonCreator + public ExplicitWithName(@JsonProperty("unrelated") String unrelated, @JsonProperty("inner") @JsonUnwrapped Inner inner) { + _unrelated = unrelated; + _inner = inner; + } + + public String getUnrelated() { + return _unrelated; + } + + public Inner getInner() { + return _inner; + } + } + + static class ImplicitWithName { + private final String _unrelated; + private final Inner _inner; + + public ImplicitWithName(@JsonProperty("unrelated") String unrelated, @JsonProperty("inner") @JsonUnwrapped Inner inner) { + _unrelated = unrelated; + _inner = inner; + } + + public String getUnrelated() { + return _unrelated; + } + + public Inner getInner() { + return _inner; + } + } + + static class WithTwoUnwrappedProperties { + private final String _unrelated; + private final Inner _inner1; + private final Inner _inner2; + + public WithTwoUnwrappedProperties( + @JsonProperty("unrelated") String unrelated, + @JsonUnwrapped(prefix = "first-") Inner inner1, + @JsonUnwrapped(prefix = "second-") Inner inner2 + ) { + _unrelated = unrelated; + _inner1 = inner1; + _inner2 = inner2; + } + + public String getUnrelated() { + return _unrelated; + } + + @JsonUnwrapped(prefix = "first-") + public Inner getInner1() { + return _inner1; + } + + @JsonUnwrapped(prefix = "second-") + public Inner getInner2() { + return _inner2; + } + } + + static class Inner { + private final String _property1; + private final String _property2; + + public Inner(@JsonProperty("property1") String property1, @JsonProperty("property2") String property2) { + _property1 = property1; + _property2 = property2; + } + + public String getProperty1() { + return _property1; + } + + public String getProperty2() { + return _property2; + } + } + + private final ObjectMapper MAPPER = newJsonMapper(); + + @Test + public void testUnwrappedWithJsonCreatorWithExplicitWithoutName() throws Exception + { + String json = "{\"unrelated\": \"unrelatedValue\", \"property1\": \"value1\", \"property2\": \"value2\"}"; + ExplicitWithoutName outer = MAPPER.readValue(json, ExplicitWithoutName.class); + + assertEquals("unrelatedValue", outer.getUnrelated()); + assertEquals("value1", outer.getInner().getProperty1()); + assertEquals("value2", outer.getInner().getProperty2()); + } + + @Test + public void testUnwrappedWithJsonCreatorExplicitWithName() throws Exception + { + String json = "{\"unrelated\": \"unrelatedValue\", \"property1\": \"value1\", \"property2\": \"value2\"}"; + ExplicitWithName outer = MAPPER.readValue(json, ExplicitWithName.class); + + assertEquals("unrelatedValue", outer.getUnrelated()); + assertEquals("value1", outer.getInner().getProperty1()); + assertEquals("value2", outer.getInner().getProperty2()); + } + + @Test + public void testUnwrappedWithJsonCreatorImplicitWithName() throws Exception + { + String json = "{\"unrelated\": \"unrelatedValue\", \"property1\": \"value1\", \"property2\": \"value2\"}"; + ImplicitWithName outer = MAPPER.readValue(json, ImplicitWithName.class); + + assertEquals("unrelatedValue", outer.getUnrelated()); + assertEquals("value1", outer.getInner().getProperty1()); + assertEquals("value2", outer.getInner().getProperty2()); + } + + @Test + public void testUnwrappedWithTwoUnwrappedProperties() throws Exception + { + String json = "{\"unrelated\": \"unrelatedValue\", " + + "\"first-property1\": \"first-value1\", \"first-property2\": \"first-value2\", " + + "\"second-property1\": \"second-value1\", \"second-property2\": \"second-value2\"}"; + WithTwoUnwrappedProperties outer = MAPPER.readValue(json, WithTwoUnwrappedProperties.class); + + assertEquals("unrelatedValue", outer.getUnrelated()); + assertEquals("first-value1", outer.getInner1().getProperty1()); + assertEquals("first-value2", outer.getInner1().getProperty2()); + assertEquals("second-value1", outer.getInner2().getProperty1()); + assertEquals("second-value2", outer.getInner2().getProperty2()); + } +} From f7e4b0bf9997e937aa844c2ddf4f560cc48be69e Mon Sep 17 00:00:00 2001 From: Tatu Saloranta Date: Tue, 12 Nov 2024 17:51:51 -0800 Subject: [PATCH 2/7] ... --- .../databind/deser/SettableBeanProperty.java | 2 ++ .../databind/deser/bean/BeanPropertyMap.java | 24 +------------------ 2 files changed, 3 insertions(+), 23 deletions(-) diff --git a/src/main/java/tools/jackson/databind/deser/SettableBeanProperty.java b/src/main/java/tools/jackson/databind/deser/SettableBeanProperty.java index e017a73154..fc8371401d 100644 --- a/src/main/java/tools/jackson/databind/deser/SettableBeanProperty.java +++ b/src/main/java/tools/jackson/databind/deser/SettableBeanProperty.java @@ -3,6 +3,7 @@ import java.lang.annotation.Annotation; import tools.jackson.core.*; +import tools.jackson.core.util.InternCache; import tools.jackson.databind.*; import tools.jackson.databind.deser.bean.BeanDeserializer; import tools.jackson.databind.deser.impl.FailingDeserializer; @@ -586,6 +587,7 @@ public final Object deserializeWith(JsonParser p, DeserializationContext ctxt, public SettableBeanProperty unwrapped(DeserializationContext ctxt, NameTransformer xf) { String newName = xf.transform(getName()); + newName = InternCache.instance.intern(newName); SettableBeanProperty renamed = withSimpleName(newName); ValueDeserializer deser = renamed.getValueDeserializer(); if (deser != null) { diff --git a/src/main/java/tools/jackson/databind/deser/bean/BeanPropertyMap.java b/src/main/java/tools/jackson/databind/deser/bean/BeanPropertyMap.java index d3ac879c81..143f6abe7a 100644 --- a/src/main/java/tools/jackson/databind/deser/bean/BeanPropertyMap.java +++ b/src/main/java/tools/jackson/databind/deser/bean/BeanPropertyMap.java @@ -4,11 +4,9 @@ import tools.jackson.core.TokenStreamFactory; import tools.jackson.core.sym.PropertyNameMatcher; -import tools.jackson.core.util.InternCache; import tools.jackson.core.util.Named; import tools.jackson.databind.DeserializationContext; import tools.jackson.databind.PropertyName; -import tools.jackson.databind.ValueDeserializer; import tools.jackson.databind.cfg.MapperConfig; import tools.jackson.databind.deser.SettableBeanProperty; import tools.jackson.databind.util.IgnorePropertiesUtil; @@ -176,7 +174,7 @@ public BeanPropertyMap renameAll(DeserializationContext ctxt, ArrayList newProps = new ArrayList(_propsInOrder.length); for (int i = 0; i < len; ++i) { SettableBeanProperty orig = _propsInOrder[i]; - SettableBeanProperty prop = _rename(ctxt, orig, transformer); + SettableBeanProperty prop = orig.unwrapped(ctxt, transformer); newProps.add(prop); } // 26-Feb-2017, tatu: Probably SHOULD handle renaming wrt Aliases? @@ -187,26 +185,6 @@ public BeanPropertyMap renameAll(DeserializationContext ctxt, .initMatcher(ctxt.tokenStreamFactory()); } - private SettableBeanProperty _rename(DeserializationContext ctxt, - SettableBeanProperty prop, NameTransformer xf) - { - if (prop != null) { - String newName = xf.transform(prop.getName()); - newName = InternCache.instance.intern(newName); - prop = prop.withSimpleName(newName); - ValueDeserializer deser = prop.getValueDeserializer(); - if (deser != null) { - @SuppressWarnings("unchecked") - ValueDeserializer newDeser = (ValueDeserializer) - deser.unwrappingDeserializer(ctxt, xf); - if (newDeser != deser) { - prop = prop.withValueDeserializer(newDeser); - } - } - } - return prop; - } - /** * Mutant factory method that will use this instance as the base, and * construct an instance that is otherwise same except for excluding From 7e3d529554b882378a72bac7b5339d53e9015453 Mon Sep 17 00:00:00 2001 From: Tatu Saloranta Date: Tue, 12 Nov 2024 18:17:54 -0800 Subject: [PATCH 3/7] Remove obsolete test wrt #265 (now "failing to fail") --- .../struct/UnwrappedCreatorParam265Test.java | 108 ------------------ 1 file changed, 108 deletions(-) delete mode 100644 src/test/java/tools/jackson/databind/struct/UnwrappedCreatorParam265Test.java diff --git a/src/test/java/tools/jackson/databind/struct/UnwrappedCreatorParam265Test.java b/src/test/java/tools/jackson/databind/struct/UnwrappedCreatorParam265Test.java deleted file mode 100644 index b4a7005e14..0000000000 --- a/src/test/java/tools/jackson/databind/struct/UnwrappedCreatorParam265Test.java +++ /dev/null @@ -1,108 +0,0 @@ -package tools.jackson.databind.struct; - -import org.junit.jupiter.api.Test; - -import com.fasterxml.jackson.annotation.*; - -import tools.jackson.databind.*; -import tools.jackson.databind.exc.InvalidDefinitionException; -import tools.jackson.databind.testutil.DatabindTestUtil; - -import static org.junit.jupiter.api.Assertions.*; - -public class UnwrappedCreatorParam265Test extends DatabindTestUtil -{ - static class JAddress { - public String address; - public String city; - public String state; - - protected JAddress() { } - - public JAddress(String address, String city, String state) { - this.address = address; - this.city = city; - this.state = state; - } - } - - static class JPersonWithoutName - { - public String name; - - protected JAddress _address; - - @JsonCreator - public JPersonWithoutName(@JsonProperty("name") String name, - @JsonUnwrapped JAddress address) - { - this.name = name; - _address = address; - } - - @JsonUnwrapped - public JAddress getAddress() { return _address; } - } - - static class JPersonWithName - { - public String name; - - protected JAddress _address; - - @JsonCreator - public JPersonWithName(@JsonProperty("name") String name, - @JsonUnwrapped - @JsonProperty("address") - JAddress address) - { - this.name = name; - _address = address; - } - - @JsonUnwrapped - public JAddress getAddress() { return _address; } - } - - /* - /********************************************************** - /* Test methods - /********************************************************** - */ - - // For [databind#265]: handle problem by throwing exception - @Test - public void testUnwrappedWithUnnamedCreatorParam() throws Exception - { - JPersonWithoutName person = new JPersonWithoutName("MyName", new JAddress("main street", "springfield", "WA")); - ObjectMapper mapper = newJsonMapper(); - // serialization should be fine as far as that goes - String json = mapper.writeValueAsString(person); - - // but not deserialization: - try { - /*JPersonWithoutName result =*/ mapper.readValue(json, JPersonWithoutName.class); - fail("Should not pass"); - } catch (InvalidDefinitionException e) { - verifyException(e, "Cannot define Creator property"); - verifyException(e, "@JsonUnwrapped"); - } - } - - // For [databind#265]: handle problem by throwing exception - @Test - public void testUnwrappedWithNamedCreatorParam() throws Exception - { - JPersonWithName person = new JPersonWithName("MyName", new JAddress("main street", "springfield", "WA")); - ObjectMapper mapper = newJsonMapper(); - // serialization should be fine as far as that goes - String json = mapper.writeValueAsString(person); - try { - /*JPersonWithName result =*/ mapper.readValue(json, JPersonWithName.class); - fail("Should not pass"); - } catch (InvalidDefinitionException e) { - verifyException(e, "Cannot define Creator property \"address\""); - verifyException(e, "@JsonUnwrapped"); - } - } -} From a715810b9e39425dd505952d92b819367d88b3fb Mon Sep 17 00:00:00 2001 From: Tatu Saloranta Date: Tue, 12 Nov 2024 18:41:06 -0800 Subject: [PATCH 4/7] Fix compilation issue: renaming happens before `BeanDeserializerBase` constructor --- .../deser/bean/BeanDeserializerBase.java | 21 ++++--------------- 1 file changed, 4 insertions(+), 17 deletions(-) diff --git a/src/main/java/tools/jackson/databind/deser/bean/BeanDeserializerBase.java b/src/main/java/tools/jackson/databind/deser/bean/BeanDeserializerBase.java index 5e6629a78d..e7714f5c20 100644 --- a/src/main/java/tools/jackson/databind/deser/bean/BeanDeserializerBase.java +++ b/src/main/java/tools/jackson/databind/deser/bean/BeanDeserializerBase.java @@ -308,6 +308,7 @@ protected BeanDeserializerBase(BeanDeserializerBase src, _valueInstantiator = src._valueInstantiator; _delegateDeserializer = src._delegateDeserializer; _arrayDelegateDeserializer = src._arrayDelegateDeserializer; + _propertyBasedCreator = src._propertyBasedCreator; _backRefs = src._backRefs; _ignorableProps = src._ignorableProps; @@ -318,24 +319,10 @@ protected BeanDeserializerBase(BeanDeserializerBase src, _objectIdReader = src._objectIdReader; _nonStandardCreation = src._nonStandardCreation; - UnwrappedPropertyHandler uph = src._unwrappedPropertyHandler; - PropertyBasedCreator pbc = src._propertyBasedCreator; - if (unwrapHandler != null) { - // delegate further unwraps, if any - if (uph != null) { // got handler, delegate - uph = uph.renameAll(unwrapHandler); - } - // and handle direct unwrapping as well: - if (pbc != null) { - pbc = pbc.renameAll(unwrapHandler); - } - _beanProperties = src._beanProperties.renameAll(unwrapHandler); - } else { - _beanProperties = src._beanProperties; - } - _propertyBasedCreator = pbc; - _unwrappedPropertyHandler = uph; + _unwrappedPropertyHandler = unwrapHandler; + _beanProperties = renamedProperties; + _needViewProcesing = src._needViewProcesing; _serializationShape = src._serializationShape; From ff049fb761c0d13d640b89a1060f63eb98b3f4c1 Mon Sep 17 00:00:00 2001 From: Tatu Saloranta Date: Wed, 13 Nov 2024 20:51:51 -0800 Subject: [PATCH 5/7] Fix naming of a test class --- ...rErrorHandling.java => BuilderErrorHandlingTest.java} | 9 +++++---- 1 file changed, 5 insertions(+), 4 deletions(-) rename src/test/java/tools/jackson/databind/deser/builder/{BuilderErrorHandling.java => BuilderErrorHandlingTest.java} (95%) diff --git a/src/test/java/tools/jackson/databind/deser/builder/BuilderErrorHandling.java b/src/test/java/tools/jackson/databind/deser/builder/BuilderErrorHandlingTest.java similarity index 95% rename from src/test/java/tools/jackson/databind/deser/builder/BuilderErrorHandling.java rename to src/test/java/tools/jackson/databind/deser/builder/BuilderErrorHandlingTest.java index 3ae67f3dd0..dd1c38225e 100644 --- a/src/test/java/tools/jackson/databind/deser/builder/BuilderErrorHandling.java +++ b/src/test/java/tools/jackson/databind/deser/builder/BuilderErrorHandlingTest.java @@ -8,12 +8,11 @@ import tools.jackson.databind.annotation.JsonDeserialize; import tools.jackson.databind.exc.MismatchedInputException; import tools.jackson.databind.exc.ValueInstantiationException; +import tools.jackson.databind.testutil.DatabindTestUtil; import static org.junit.jupiter.api.Assertions.*; -import static tools.jackson.databind.testutil.DatabindTestUtil.*; - -public class BuilderErrorHandling +public class BuilderErrorHandlingTest extends DatabindTestUtil { @JsonDeserialize(builder=SimpleBuilderXY.class) static class ValueClassXY @@ -116,7 +115,9 @@ public void testUnknownProperty() throws Exception // first, default failure String json = a2q("{'x':1,'z':2,'y':4}"); try { - MAPPER.readValue(json, ValueClassXY.class); + MAPPER.readerFor(ValueClassXY.class) + .with(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES) + .readValue(json); fail("Should not pass"); } catch (MismatchedInputException e) { verifyException(e, "Unrecognized property "); From ac149f217309c909cf3768b48475bff1080f2137 Mon Sep 17 00:00:00 2001 From: Tatu Saloranta Date: Wed, 13 Nov 2024 20:55:53 -0800 Subject: [PATCH 6/7] Mark one of 2 failing tests as "tofix" --- .../UnwrappedWithPrefixCreator1467Test.java} | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) rename src/test/java/tools/jackson/databind/{struct/UnwrappedPropertyBasedCreatorWithPrefixTest.java => tofix/UnwrappedWithPrefixCreator1467Test.java} (77%) diff --git a/src/test/java/tools/jackson/databind/struct/UnwrappedPropertyBasedCreatorWithPrefixTest.java b/src/test/java/tools/jackson/databind/tofix/UnwrappedWithPrefixCreator1467Test.java similarity index 77% rename from src/test/java/tools/jackson/databind/struct/UnwrappedPropertyBasedCreatorWithPrefixTest.java rename to src/test/java/tools/jackson/databind/tofix/UnwrappedWithPrefixCreator1467Test.java index c685047b02..2fe43baf4c 100644 --- a/src/test/java/tools/jackson/databind/struct/UnwrappedPropertyBasedCreatorWithPrefixTest.java +++ b/src/test/java/tools/jackson/databind/tofix/UnwrappedWithPrefixCreator1467Test.java @@ -1,4 +1,4 @@ -package tools.jackson.databind.struct; +package tools.jackson.databind.tofix; import org.junit.jupiter.api.Test; @@ -6,10 +6,12 @@ import com.fasterxml.jackson.annotation.JsonUnwrapped; 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.*; -public class UnwrappedPropertyBasedCreatorWithPrefixTest extends DatabindTestUtil +// [databind#1467]: works for 2.19+, fails for 3.0 for some reason +public class UnwrappedWithPrefixCreator1467Test extends DatabindTestUtil { static class Outer { @JsonUnwrapped(prefix = "inner-") @@ -30,6 +32,7 @@ public String getProperty() { private final ObjectMapper MAPPER = newJsonMapper(); + @JacksonTestFailureExpected @Test public void testUnwrappedWithJsonCreatorWithExplicitWithoutName() throws Exception { From fe1068def06f24d336612151cd6a3ab9e7c011ee Mon Sep 17 00:00:00 2001 From: Tatu Saloranta Date: Wed, 13 Nov 2024 20:57:35 -0800 Subject: [PATCH 7/7] Move the last failing test under "tofix" --- .../UnwrappedWithCreator1467Test.java} | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) rename src/test/java/tools/jackson/databind/{struct/UnwrappedWithCreatorTest.java => tofix/UnwrappedWithCreator1467Test.java} (95%) diff --git a/src/test/java/tools/jackson/databind/struct/UnwrappedWithCreatorTest.java b/src/test/java/tools/jackson/databind/tofix/UnwrappedWithCreator1467Test.java similarity index 95% rename from src/test/java/tools/jackson/databind/struct/UnwrappedWithCreatorTest.java rename to src/test/java/tools/jackson/databind/tofix/UnwrappedWithCreator1467Test.java index d733c58b30..baa7004f98 100644 --- a/src/test/java/tools/jackson/databind/struct/UnwrappedWithCreatorTest.java +++ b/src/test/java/tools/jackson/databind/tofix/UnwrappedWithCreator1467Test.java @@ -1,4 +1,4 @@ -package tools.jackson.databind.struct; +package tools.jackson.databind.tofix; import org.junit.jupiter.api.Test; @@ -6,13 +6,14 @@ import tools.jackson.databind.*; import tools.jackson.databind.testutil.DatabindTestUtil; +import tools.jackson.databind.testutil.failure.JacksonTestFailureExpected; import static org.junit.jupiter.api.Assertions.*; /** * Tests to verify [databind#1467]. */ -public class UnwrappedWithCreatorTest extends DatabindTestUtil +public class UnwrappedWithCreator1467Test extends DatabindTestUtil { static class ExplicitWithoutName { private final String _unrelated; @@ -154,6 +155,8 @@ public void testUnwrappedWithJsonCreatorImplicitWithName() throws Exception assertEquals("value2", outer.getInner().getProperty2()); } + // [databind#1467]: works for 2.19+, fails for 3.0 for some reason + @JacksonTestFailureExpected @Test public void testUnwrappedWithTwoUnwrappedProperties() throws Exception {