From 10e60b9aacd22bff58e4447a5619f2ce44dd3d84 Mon Sep 17 00:00:00 2001 From: "Kim, Joo Hyuk" Date: Sat, 1 Jun 2024 13:39:28 +0900 Subject: [PATCH 01/24] Implement all --- .../deser/BasicDeserializerFactory.java | 14 +- .../databind/deser/BeanDeserializer.java | 4 +- .../deser/BeanDeserializerFactory.java | 41 ++++- .../databind/deser/SettableAnyProperty.java | 65 ++++++++ .../deser/impl/PropertyBasedCreator.java | 12 ++ .../databind/deser/impl/PropertyValue.java | 27 ++++ .../deser/impl/PropertyValueBuffer.java | 52 ++++++- .../creators/AnySetterForCreator562Test.java | 145 ++++++++++++++++++ 8 files changed, 350 insertions(+), 10 deletions(-) create mode 100644 src/test/java/com/fasterxml/jackson/databind/deser/creators/AnySetterForCreator562Test.java diff --git a/src/main/java/com/fasterxml/jackson/databind/deser/BasicDeserializerFactory.java b/src/main/java/com/fasterxml/jackson/databind/deser/BasicDeserializerFactory.java index 71a0f635ee..ba3df26ba5 100644 --- a/src/main/java/com/fasterxml/jackson/databind/deser/BasicDeserializerFactory.java +++ b/src/main/java/com/fasterxml/jackson/databind/deser/BasicDeserializerFactory.java @@ -508,20 +508,28 @@ private void _addSelectedPropertiesBasedCreator(DeserializationContext ctxt, { final int paramCount = candidate.paramCount(); SettableBeanProperty[] properties = new SettableBeanProperty[paramCount]; + boolean anySetterFound = false; for (int i = 0; i < paramCount; ++i) { JacksonInject.Value injectId = candidate.injection(i); AnnotatedParameter param = candidate.parameter(i); PropertyName name = candidate.paramName(i); - if (name == null) { + boolean isAnySetter = Boolean.TRUE.equals(ctxt.getAnnotationIntrospector().hasAnySetter(param)); + if (isAnySetter) { + if (anySetterFound) { + ctxt.reportBadTypeDefinition(beanDesc, "More than one 'any-setter' specified"); + } else { + anySetterFound = true; + } + } else if (name == null) { // 21-Sep-2017, tatu: Looks like we want to block accidental use of Unwrapped, // as that will not work with Creators well at all NameTransformer unwrapper = ctxt.getAnnotationIntrospector().findUnwrappingNameTransformer(param); if (unwrapper != null) { _reportUnwrappedCreatorProperty(ctxt, beanDesc, param); } - // Must be injectable or have name; without either won't work - if ((name == null) && (injectId == null)) { + if ((name == null) && (injectId == null)) { + // Must be injectable or have name; without either won't work ctxt.reportBadTypeDefinition(beanDesc, "Argument #%d of Creator %s has no property name (and is not Injectable): can not use as property-based Creator", i, candidate); diff --git a/src/main/java/com/fasterxml/jackson/databind/deser/BeanDeserializer.java b/src/main/java/com/fasterxml/jackson/databind/deser/BeanDeserializer.java index cf744fe359..7656dc12b5 100644 --- a/src/main/java/com/fasterxml/jackson/databind/deser/BeanDeserializer.java +++ b/src/main/java/com/fasterxml/jackson/databind/deser/BeanDeserializer.java @@ -415,7 +415,9 @@ protected Object _deserializeUsingPropertyBased(final JsonParser p, final Deseri throws IOException { final PropertyBasedCreator creator = _propertyBasedCreator; - PropertyValueBuffer buffer = creator.startBuilding(p, ctxt, _objectIdReader); + PropertyValueBuffer buffer = (_anySetter != null) + ? creator.startBuildingWithAnySetter(p, ctxt, _objectIdReader, _anySetter) + : creator.startBuilding(p, ctxt, _objectIdReader); TokenBuffer unknown = null; final Class activeView = _needViewProcesing ? ctxt.getActiveView() : null; diff --git a/src/main/java/com/fasterxml/jackson/databind/deser/BeanDeserializerFactory.java b/src/main/java/com/fasterxml/jackson/databind/deser/BeanDeserializerFactory.java index 03f4d26f42..7dc7c9826c 100644 --- a/src/main/java/com/fasterxml/jackson/databind/deser/BeanDeserializerFactory.java +++ b/src/main/java/com/fasterxml/jackson/databind/deser/BeanDeserializerFactory.java @@ -544,8 +544,11 @@ protected void addBeanProps(DeserializationContext ctxt, // Also, do we have a fallback "any" setter? AnnotatedMember anySetter = beanDesc.findAnySetterAccessor(); + AnnotatedMember creatorPropWithAnySetter = _findCreatorPropWithAnySetter(creatorProps, ctxt.getAnnotationIntrospector()); if (anySetter != null) { builder.setAnySetter(constructAnySetter(ctxt, beanDesc, anySetter)); + } else if (creatorPropWithAnySetter != null) { + builder.setAnySetter(constructAnySetter(ctxt, beanDesc, creatorPropWithAnySetter)); } else { // 23-Jan-2018, tatu: although [databind#1805] would suggest we should block // properties regardless, for now only consider unless there's any setter... @@ -661,6 +664,19 @@ protected void addBeanProps(DeserializationContext ctxt, } } + private AnnotatedMember _findCreatorPropWithAnySetter(SettableBeanProperty[] creatorProps, AnnotationIntrospector ai) + { + if (creatorProps != null) { + for (SettableBeanProperty prop : creatorProps) { + AnnotatedMember member = prop.getMember(); + if (member != null && Boolean.TRUE.equals(ai.hasAnySetter(member))) { + return member; + } + } + } + return null; + } + private boolean _isSetterlessType(Class rawType) { // May also need to consider getters // for Map/Collection properties; but with lowest precedence @@ -818,7 +834,21 @@ protected SettableAnyProperty constructAnySetter(DeserializationContext ctxt, prop = new BeanProperty.Std(PropertyName.construct(mutator.getName()), valueType, null, mutator, PropertyMetadata.STD_OPTIONAL); - + // [databind#562] Allow @JsonAnySetter in creators + } else if (mutator instanceof AnnotatedParameter){ + AnnotatedParameter af = (AnnotatedParameter) mutator; + // get the type from the content type of the map object + JavaType fieldType = af.getType(); + if (!fieldType.isMapLikeType()) { + return ctxt.reportBadDefinition(beanDesc.getType(), String.format( + "Unsupported type for any-setter: %s -- only support `Map`s, `JsonNode` and `ObjectNode` ", + ClassUtil.getTypeDescription(fieldType))); + } + fieldType = resolveMemberAndTypeAnnotations(ctxt, mutator, fieldType); + keyType = fieldType.getKeyType(); + valueType = fieldType.getContentType(); + prop = new BeanProperty.Std(PropertyName.construct(mutator.getName()), + fieldType, null, mutator, PropertyMetadata.STD_OPTIONAL); } else if (isField) { AnnotatedField af = (AnnotatedField) mutator; // get the type from the content type of the map object @@ -879,9 +909,14 @@ protected SettableAnyProperty constructAnySetter(DeserializationContext ctxt, if (isField) { return SettableAnyProperty.constructForMapField(ctxt, prop, mutator, valueType, keyDeser, deser, typeDeser); - } - return SettableAnyProperty.constructForMethod(ctxt, + } else if (mutator instanceof AnnotatedParameter) { + // [databind#562] Allow @JsonAnySetter in creators + return SettableAnyProperty.constructForMapParameter(ctxt, + prop, mutator, valueType, keyDeser, deser, typeDeser); + } else { + return SettableAnyProperty.constructForMethod(ctxt, prop, mutator, valueType, keyDeser, deser, typeDeser); + } } /** diff --git a/src/main/java/com/fasterxml/jackson/databind/deser/SettableAnyProperty.java b/src/main/java/com/fasterxml/jackson/databind/deser/SettableAnyProperty.java index b7aafa205b..249ab2e2b3 100644 --- a/src/main/java/com/fasterxml/jackson/databind/deser/SettableAnyProperty.java +++ b/src/main/java/com/fasterxml/jackson/databind/deser/SettableAnyProperty.java @@ -3,6 +3,7 @@ import java.io.IOException; import java.util.LinkedHashMap; import java.util.Map; +import java.util.Objects; import com.fasterxml.jackson.core.*; import com.fasterxml.jackson.databind.*; @@ -117,6 +118,29 @@ public static SettableAnyProperty constructForJsonNodeField(DeserializationConte ctxt.getNodeFactory()); } + /** + * @since 2.18 + */ + public static SettableAnyProperty constructForMapParameter(DeserializationContext ctxt, + BeanProperty property, + AnnotatedMember field, JavaType valueType, + KeyDeserializer keyDeser, + JsonDeserializer valueDeser, TypeDeserializer typeDeser) + { + Class mapType = field.getRawType(); + // 02-Aug-2022, tatu: Ideally would be resolved to a concrete type by caller but + // alas doesn't appear to happen. Nor does `BasicDeserializerFactory` expose method + // for finding default or explicit mappings. + if (mapType == Map.class) { + mapType = LinkedHashMap.class; + } + ValueInstantiator vi = JDKValueInstantiators.findStdValueInstantiator(ctxt.getConfig(), mapType); + return new MapParamAnyProperty(property, field, valueType, + keyDeser, valueDeser, typeDeser, + vi); + } + + // Abstract @since 2.14 public abstract SettableAnyProperty withValueDeserializer(JsonDeserializer deser); @@ -437,4 +461,45 @@ public SettableAnyProperty withValueDeserializer(JsonDeserializer deser) return this; } } + + + /** + * [databind#562] Allow @JsonAnySetter on Creator constructor + * + * @since 2.18 + */ + protected static class MapParamAnyProperty extends SettableAnyProperty + implements java.io.Serializable + { + private static final long serialVersionUID = 1L; + + protected final ValueInstantiator _valueInstantiator; + + public MapParamAnyProperty(BeanProperty property, + AnnotatedMember field, JavaType valueType, + KeyDeserializer keyDeser, + JsonDeserializer valueDeser, TypeDeserializer typeDeser, + ValueInstantiator inst) + { + super(property, field, valueType, + keyDeser, valueDeser, typeDeser); + _valueInstantiator = Objects.requireNonNull(inst, "ValueInstantiator for MapParameterAnyProperty cannot be `null`"); + } + + @Override + public SettableAnyProperty withValueDeserializer(JsonDeserializer deser) + { + return new MapParamAnyProperty(_property, _setter, _type, + _keyDeserializer, deser, _valueTypeDeserializer, + _valueInstantiator); + } + + @Override + protected void _set(Object instance, Object propName, Object value) + { + ((Map) instance).put(propName, value); + } + + } + } diff --git a/src/main/java/com/fasterxml/jackson/databind/deser/impl/PropertyBasedCreator.java b/src/main/java/com/fasterxml/jackson/databind/deser/impl/PropertyBasedCreator.java index 5225baaab6..c97e61e9bc 100644 --- a/src/main/java/com/fasterxml/jackson/databind/deser/impl/PropertyBasedCreator.java +++ b/src/main/java/com/fasterxml/jackson/databind/deser/impl/PropertyBasedCreator.java @@ -6,6 +6,7 @@ import com.fasterxml.jackson.core.JsonParser; import com.fasterxml.jackson.databind.*; +import com.fasterxml.jackson.databind.deser.SettableAnyProperty; import com.fasterxml.jackson.databind.deser.SettableBeanProperty; import com.fasterxml.jackson.databind.deser.ValueInstantiator; @@ -214,6 +215,17 @@ public Object build(DeserializationContext ctxt, PropertyValueBuffer buffer) thr return bean; } + /** + * Method called when starting to build a bean instance. + * + * @since 2.18 (added SettableAnyProperty parameter) + */ + public PropertyValueBuffer startBuildingWithAnySetter(JsonParser p, DeserializationContext ctxt, + ObjectIdReader oir, SettableAnyProperty anySetter) { + return new PropertyValueBuffer(p, ctxt, _propertyCount, oir, anySetter); + } + + /* /********************************************************** /* Helper classes diff --git a/src/main/java/com/fasterxml/jackson/databind/deser/impl/PropertyValue.java b/src/main/java/com/fasterxml/jackson/databind/deser/impl/PropertyValue.java index 6558b3dcf3..34b4de775e 100644 --- a/src/main/java/com/fasterxml/jackson/databind/deser/impl/PropertyValue.java +++ b/src/main/java/com/fasterxml/jackson/databind/deser/impl/PropertyValue.java @@ -24,6 +24,10 @@ protected PropertyValue(PropertyValue next, Object value) this.value = value; } + public static PropertyValue empty() { + return Empty.INSTANCE; + } + /** * Method called to assign stored value of this property to specified * bean instance @@ -113,4 +117,27 @@ public void assign(Object bean) ((java.util.Map) bean).put(_key, value); } } + + /** + * Property value type used do nothing + */ + private final static class Empty + extends PropertyValue + { + + public static final PropertyValue INSTANCE = new Empty(); + + private Empty() + { + super(null, null); + } + + @SuppressWarnings("unchecked") + @Override + public void assign(Object bean) + throws IOException + { + // no-op + } + } } diff --git a/src/main/java/com/fasterxml/jackson/databind/deser/impl/PropertyValueBuffer.java b/src/main/java/com/fasterxml/jackson/databind/deser/impl/PropertyValueBuffer.java index 5d323381a0..aa907d335d 100644 --- a/src/main/java/com/fasterxml/jackson/databind/deser/impl/PropertyValueBuffer.java +++ b/src/main/java/com/fasterxml/jackson/databind/deser/impl/PropertyValueBuffer.java @@ -2,6 +2,8 @@ import java.io.IOException; import java.util.BitSet; +import java.util.HashMap; +import java.util.Map; import com.fasterxml.jackson.core.JsonParser; @@ -75,14 +77,29 @@ public class PropertyValueBuffer */ protected Object _idValue; + /** + * [databind#562]: Allow use of `@JsonAnySetter` in `@JsonCreator` + * + * @since 2.18 + */ + protected final SettableAnyProperty _anySetter; + + /** + * Flag that indicates whether this buffer has been consumed. + */ + protected boolean _consumed; + /* /********************************************************** /* Life-cycle /********************************************************** */ + /** + * @since 2.18 + */ public PropertyValueBuffer(JsonParser p, DeserializationContext ctxt, int paramCount, - ObjectIdReader oir) + ObjectIdReader oir, SettableAnyProperty anySetter) { _parser = p; _context = ctxt; @@ -94,8 +111,16 @@ public PropertyValueBuffer(JsonParser p, DeserializationContext ctxt, int paramC } else { _paramsSeenBig = new BitSet(); } + _anySetter = anySetter; + } + + public PropertyValueBuffer(JsonParser p, DeserializationContext ctxt, int paramCount, + ObjectIdReader oir) + { + this(p, ctxt, paramCount, oir, null); } + /** * Returns {@code true} if the given property was seen in the JSON source by * this buffer. @@ -145,7 +170,7 @@ public Object getParameter(SettableBeanProperty prop) * then whole JSON Object has been processed, */ public Object[] getParameters(SettableBeanProperty[] props) - throws JsonMappingException + throws JsonMappingException, IOException { // quick check to see if anything else is needed if (_paramsNeeded > 0) { @@ -176,6 +201,22 @@ public Object[] getParameters(SettableBeanProperty[] props) } } } + + // [databind#562] since 2.18 : Respect @JsonAnySetter in @JsonCreator + if (_anySetter != null) { + for (int i = 0; i < _creatorParameters.length; i++) { + if (_creatorParameters[i] == null) { + // anySetter should know about index + Map anySetterMap = new HashMap<>(); + for (PropertyValue pv = this.buffered(); pv != null; pv = pv.next) { + pv.assign(anySetterMap); + } + _creatorParameters[i] = anySetterMap; + _consumed = true; + break; + } + } + } return _creatorParameters; } @@ -261,7 +302,12 @@ public Object handleIdValue(final DeserializationContext ctxt, Object bean) thro return bean; } - protected PropertyValue buffered() { return _buffered; } + protected PropertyValue buffered() { + if (_consumed) { + return PropertyValue.empty(); + } + return _buffered; + } public boolean isComplete() { return _paramsNeeded <= 0; } diff --git a/src/test/java/com/fasterxml/jackson/databind/deser/creators/AnySetterForCreator562Test.java b/src/test/java/com/fasterxml/jackson/databind/deser/creators/AnySetterForCreator562Test.java new file mode 100644 index 0000000000..aef766f9ca --- /dev/null +++ b/src/test/java/com/fasterxml/jackson/databind/deser/creators/AnySetterForCreator562Test.java @@ -0,0 +1,145 @@ +package com.fasterxml.jackson.databind.deser.creators; + +import java.util.HashMap; +import java.util.Map; + +import com.fasterxml.jackson.annotation.JsonAnySetter; +import com.fasterxml.jackson.annotation.JsonCreator; +import com.fasterxml.jackson.annotation.JsonProperty; +import com.fasterxml.jackson.databind.DeserializationFeature; +import com.fasterxml.jackson.databind.ObjectMapper; +import com.fasterxml.jackson.databind.exc.InvalidDefinitionException; +import com.fasterxml.jackson.databind.exc.MismatchedInputException; +import com.fasterxml.jackson.databind.testutil.DatabindTestUtil; + +import org.junit.jupiter.api.Test; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.fail; + +public class AnySetterForCreator562Test extends DatabindTestUtil +{ + // [databind#562] + static class POJO562 + { + String a; + + Map stuff; + + @JsonCreator + public POJO562(@JsonProperty("a") String a, + @JsonAnySetter Map leftovers + ) { + this.a = a; + stuff = leftovers; + } + } + + // [databind#562]: failing case + static class MultipleAny562 + { + @JsonCreator + public MultipleAny562(@JsonProperty("a") String a, + @JsonAnySetter Map leftovers, + @JsonAnySetter Map leftovers2) { + throw new Error("Should never get here!"); + } + } + + // [databind#562] + static class PojoWithDisabled + { + String a; + + Map stuff; + + @JsonCreator + public PojoWithDisabled(@JsonProperty("a") String a, + @JsonAnySetter(enabled = false) Map leftovers + ) { + this.a = a; + stuff = leftovers; + } + } + + private final ObjectMapper MAPPER = newJsonMapper(); + + // [databind#562] + @Test + public void anySetterViaCreator562() throws Exception + { + Map expected = new HashMap<>(); + expected.put("b", Integer.valueOf(42)); + expected.put("c", Integer.valueOf(111)); + + POJO562 pojo = MAPPER.readValue(a2q( + "{'a':'value', 'b':42, 'c': 111}" + ), + POJO562.class); + + assertEquals("value", pojo.a); + assertEquals(expected, pojo.stuff); + } + + // [databind#562] + @Test + public void testWithFailureConfigs562() throws Exception + { + ObjectMapper failOnNullMapper = jsonMapperBuilder() + .enable(DeserializationFeature.FAIL_ON_NULL_CREATOR_PROPERTIES).build(); + + try { + failOnNullMapper.readValue(a2q("{'a':'value'}"), POJO562.class); + fail("Should not pass"); + } catch (MismatchedInputException e) { + verifyException(e, "Null value for creator property ''"); + } + + ObjectMapper failOnMissingMapper = jsonMapperBuilder() + .enable(DeserializationFeature.FAIL_ON_MISSING_CREATOR_PROPERTIES).build(); + try { + failOnMissingMapper.readValue(a2q("{'a':'value'}"), POJO562.class); + fail("Should not pass"); + } catch (MismatchedInputException e) { + verifyException(e, "Missing creator property ''"); + } + + ObjectMapper failOnBothMapper = jsonMapperBuilder() + .enable(DeserializationFeature.FAIL_ON_NULL_CREATOR_PROPERTIES) + .enable(DeserializationFeature.FAIL_ON_MISSING_CREATOR_PROPERTIES) + .build(); + try { + failOnBothMapper.readValue(a2q("{'a':'value'}"), POJO562.class); + fail("Should not pass"); + } catch (MismatchedInputException e) { + verifyException(e, "Missing creator property ''"); + } + } + + // [databind#562] + @Test + public void testAnySetterViaCreator562FailForDup() throws Exception + { + try { + MAPPER.readValue("{}", MultipleAny562.class); + fail("Should not pass"); + } catch (InvalidDefinitionException e) { + verifyException(e, "Invalid type definition"); + verifyException(e, "More than one 'any-setter'"); + } + } + + // [databind#562] + @Test + public void testAnySetterViaCreator562Disabled() throws Exception + { + try { + MAPPER.readValue(a2q("{'a':'value', 'b':42, 'c': 111}"), + PojoWithDisabled.class); + fail(); + } catch (InvalidDefinitionException e) { + verifyException(e, "Invalid type definition for type"); + verifyException(e, "has no property name (and is not Injectable): can not use as property-based Creator"); + } + } +} \ No newline at end of file From b9b43f356f4553f617e9f6965e8a1ae27447e544 Mon Sep 17 00:00:00 2001 From: "Kim, Joo Hyuk" Date: Sat, 1 Jun 2024 13:47:35 +0900 Subject: [PATCH 02/24] Order methods in PropertyBasedCreator --- .../deser/impl/PropertyBasedCreator.java | 23 +++++++++---------- .../deser/impl/PropertyValueBuffer.java | 1 + 2 files changed, 12 insertions(+), 12 deletions(-) diff --git a/src/main/java/com/fasterxml/jackson/databind/deser/impl/PropertyBasedCreator.java b/src/main/java/com/fasterxml/jackson/databind/deser/impl/PropertyBasedCreator.java index c97e61e9bc..b2fd6e7e9a 100644 --- a/src/main/java/com/fasterxml/jackson/databind/deser/impl/PropertyBasedCreator.java +++ b/src/main/java/com/fasterxml/jackson/databind/deser/impl/PropertyBasedCreator.java @@ -195,7 +195,17 @@ public SettableBeanProperty findCreatorProperty(int propertyIndex) { */ public PropertyValueBuffer startBuilding(JsonParser p, DeserializationContext ctxt, ObjectIdReader oir) { - return new PropertyValueBuffer(p, ctxt, _propertyCount, oir); + return new PropertyValueBuffer(p, ctxt, _propertyCount, oir, null); + } + + /** + * Method called when starting to build a bean instance. + * + * @since 2.18 (added SettableAnyProperty parameter) + */ + public PropertyValueBuffer startBuildingWithAnySetter(JsonParser p, DeserializationContext ctxt, + ObjectIdReader oir, SettableAnyProperty anySetter) { + return new PropertyValueBuffer(p, ctxt, _propertyCount, oir, anySetter); } public Object build(DeserializationContext ctxt, PropertyValueBuffer buffer) throws IOException @@ -215,17 +225,6 @@ public Object build(DeserializationContext ctxt, PropertyValueBuffer buffer) thr return bean; } - /** - * Method called when starting to build a bean instance. - * - * @since 2.18 (added SettableAnyProperty parameter) - */ - public PropertyValueBuffer startBuildingWithAnySetter(JsonParser p, DeserializationContext ctxt, - ObjectIdReader oir, SettableAnyProperty anySetter) { - return new PropertyValueBuffer(p, ctxt, _propertyCount, oir, anySetter); - } - - /* /********************************************************** /* Helper classes diff --git a/src/main/java/com/fasterxml/jackson/databind/deser/impl/PropertyValueBuffer.java b/src/main/java/com/fasterxml/jackson/databind/deser/impl/PropertyValueBuffer.java index aa907d335d..c1b45db8e7 100644 --- a/src/main/java/com/fasterxml/jackson/databind/deser/impl/PropertyValueBuffer.java +++ b/src/main/java/com/fasterxml/jackson/databind/deser/impl/PropertyValueBuffer.java @@ -114,6 +114,7 @@ public PropertyValueBuffer(JsonParser p, DeserializationContext ctxt, int paramC _anySetter = anySetter; } + @Deprecated // since 2.18 public PropertyValueBuffer(JsonParser p, DeserializationContext ctxt, int paramCount, ObjectIdReader oir) { From 2ec9140668c4bf98b738331224f94b185e1aabc0 Mon Sep 17 00:00:00 2001 From: "Kim, Joo Hyuk" Date: Sat, 1 Jun 2024 14:05:35 +0900 Subject: [PATCH 03/24] Clean up anySetter discovery during building beanDeserialier.addBeanProps() --- .../deser/BeanDeserializerFactory.java | 24 ++++++++++++------- 1 file changed, 16 insertions(+), 8 deletions(-) diff --git a/src/main/java/com/fasterxml/jackson/databind/deser/BeanDeserializerFactory.java b/src/main/java/com/fasterxml/jackson/databind/deser/BeanDeserializerFactory.java index 7dc7c9826c..e4da55876e 100644 --- a/src/main/java/com/fasterxml/jackson/databind/deser/BeanDeserializerFactory.java +++ b/src/main/java/com/fasterxml/jackson/databind/deser/BeanDeserializerFactory.java @@ -543,12 +543,9 @@ protected void addBeanProps(DeserializationContext ctxt, } // Also, do we have a fallback "any" setter? - AnnotatedMember anySetter = beanDesc.findAnySetterAccessor(); - AnnotatedMember creatorPropWithAnySetter = _findCreatorPropWithAnySetter(creatorProps, ctxt.getAnnotationIntrospector()); + SettableAnyProperty anySetter = _resolveAnySetter(ctxt, beanDesc, creatorProps); if (anySetter != null) { - builder.setAnySetter(constructAnySetter(ctxt, beanDesc, anySetter)); - } else if (creatorPropWithAnySetter != null) { - builder.setAnySetter(constructAnySetter(ctxt, beanDesc, creatorPropWithAnySetter)); + builder.setAnySetter(anySetter); } else { // 23-Jan-2018, tatu: although [databind#1805] would suggest we should block // properties regardless, for now only consider unless there's any setter... @@ -664,16 +661,25 @@ protected void addBeanProps(DeserializationContext ctxt, } } - private AnnotatedMember _findCreatorPropWithAnySetter(SettableBeanProperty[] creatorProps, AnnotationIntrospector ai) + // since 2.18 + private SettableAnyProperty _resolveAnySetter(DeserializationContext ctxt, BeanDescription beanDesc, + SettableBeanProperty[] creatorProps) throws JsonMappingException { + // Find the regular method/field level any-setter + AnnotatedMember anySetter = beanDesc.findAnySetterAccessor(); + if (anySetter != null) { + return constructAnySetter(ctxt, beanDesc, anySetter); + } + // else look for any-setter via @JsonCreator if (creatorProps != null) { for (SettableBeanProperty prop : creatorProps) { AnnotatedMember member = prop.getMember(); - if (member != null && Boolean.TRUE.equals(ai.hasAnySetter(member))) { - return member; + if (member != null && Boolean.TRUE.equals(ctxt.getAnnotationIntrospector().hasAnySetter(member))) { + return constructAnySetter(ctxt, beanDesc, member); } } } + // not found, that's fine, too return null; } @@ -919,6 +925,8 @@ protected SettableAnyProperty constructAnySetter(DeserializationContext ctxt, } } + + /** * Method that will construct a regular bean property setter using * the given setter method. From ee9afc01e17e8c9a002a3e0187b77c953f5d15de Mon Sep 17 00:00:00 2001 From: "Kim, Joo Hyuk" Date: Sat, 1 Jun 2024 16:29:26 +0900 Subject: [PATCH 04/24] Implement separate constructCreatorPropertyAnySetter() --- .../deser/BeanDeserializerFactory.java | 81 +++++++++++++------ 1 file changed, 58 insertions(+), 23 deletions(-) diff --git a/src/main/java/com/fasterxml/jackson/databind/deser/BeanDeserializerFactory.java b/src/main/java/com/fasterxml/jackson/databind/deser/BeanDeserializerFactory.java index e4da55876e..ca3035e940 100644 --- a/src/main/java/com/fasterxml/jackson/databind/deser/BeanDeserializerFactory.java +++ b/src/main/java/com/fasterxml/jackson/databind/deser/BeanDeserializerFactory.java @@ -675,7 +675,7 @@ private SettableAnyProperty _resolveAnySetter(DeserializationContext ctxt, BeanD for (SettableBeanProperty prop : creatorProps) { AnnotatedMember member = prop.getMember(); if (member != null && Boolean.TRUE.equals(ctxt.getAnnotationIntrospector().hasAnySetter(member))) { - return constructAnySetter(ctxt, beanDesc, member); + return constructCreatorPropertyAnySetter(ctxt, beanDesc, member); } } } @@ -840,21 +840,7 @@ protected SettableAnyProperty constructAnySetter(DeserializationContext ctxt, prop = new BeanProperty.Std(PropertyName.construct(mutator.getName()), valueType, null, mutator, PropertyMetadata.STD_OPTIONAL); - // [databind#562] Allow @JsonAnySetter in creators - } else if (mutator instanceof AnnotatedParameter){ - AnnotatedParameter af = (AnnotatedParameter) mutator; - // get the type from the content type of the map object - JavaType fieldType = af.getType(); - if (!fieldType.isMapLikeType()) { - return ctxt.reportBadDefinition(beanDesc.getType(), String.format( - "Unsupported type for any-setter: %s -- only support `Map`s, `JsonNode` and `ObjectNode` ", - ClassUtil.getTypeDescription(fieldType))); - } - fieldType = resolveMemberAndTypeAnnotations(ctxt, mutator, fieldType); - keyType = fieldType.getKeyType(); - valueType = fieldType.getContentType(); - prop = new BeanProperty.Std(PropertyName.construct(mutator.getName()), - fieldType, null, mutator, PropertyMetadata.STD_OPTIONAL); + } else if (isField) { AnnotatedField af = (AnnotatedField) mutator; // get the type from the content type of the map object @@ -915,16 +901,65 @@ protected SettableAnyProperty constructAnySetter(DeserializationContext ctxt, if (isField) { return SettableAnyProperty.constructForMapField(ctxt, prop, mutator, valueType, keyDeser, deser, typeDeser); - } else if (mutator instanceof AnnotatedParameter) { - // [databind#562] Allow @JsonAnySetter in creators - return SettableAnyProperty.constructForMapParameter(ctxt, - prop, mutator, valueType, keyDeser, deser, typeDeser); - } else { - return SettableAnyProperty.constructForMethod(ctxt, - prop, mutator, valueType, keyDeser, deser, typeDeser); } + return SettableAnyProperty.constructForMethod(ctxt, + prop, mutator, valueType, keyDeser, deser, typeDeser); } + /** + * @param mutator Instance of {@link AnnotatedParameter} that should represent any-setter parameter. + */ + @SuppressWarnings("unchecked") + protected SettableAnyProperty constructCreatorPropertyAnySetter(DeserializationContext ctxt, + BeanDescription beanDesc, AnnotatedMember mutator) + throws JsonMappingException + { + if (!(mutator instanceof AnnotatedParameter)){ + ctxt.reportBadDefinition(beanDesc.getType(), String.format( + "Unrecognized mutator type for any-setter: %s", + ClassUtil.nameOf(mutator.getClass()))); + } + AnnotatedParameter af = (AnnotatedParameter) mutator; + // get the type from the content type of the map object + JavaType fieldType = af.getType(); + if (!fieldType.isMapLikeType()) { + return ctxt.reportBadDefinition(beanDesc.getType(), String.format( + "Unsupported type for any-setter: %s -- only support `Map`s, `JsonNode` and `ObjectNode` ", + ClassUtil.getTypeDescription(fieldType))); + } + fieldType = resolveMemberAndTypeAnnotations(ctxt, mutator, fieldType); + JavaType keyType = fieldType.getKeyType(); + JavaType valueType = fieldType.getContentType(); + BeanProperty prop = new BeanProperty.Std(PropertyName.construct(mutator.getName()), + fieldType, null, mutator, PropertyMetadata.STD_OPTIONAL); + + // First: see if there are explicitly specified + // and then possible direct deserializer override on accessor + KeyDeserializer keyDeser = findKeyDeserializerFromAnnotation(ctxt, mutator); + if (keyDeser == null) { + keyDeser = keyType.getValueHandler(); + } + if (keyDeser == null) { + keyDeser = ctxt.findKeyDeserializer(keyType, prop); + } else { + if (keyDeser instanceof ContextualKeyDeserializer) { + keyDeser = ((ContextualKeyDeserializer) keyDeser) + .createContextual(ctxt, prop); + } + } + JsonDeserializer deser = findContentDeserializerFromAnnotation(ctxt, mutator); + if (deser == null) { + deser = valueType.getValueHandler(); + } + if (deser != null) { + // As per [databind#462] need to ensure we contextualize deserializer before passing it on + deser = (JsonDeserializer) ctxt.handlePrimaryContextualization(deser, prop, valueType); + } + TypeDeserializer typeDeser = valueType.getTypeHandler(); + // [databind#562] Allow @JsonAnySetter in creators + return SettableAnyProperty.constructForMapParameter(ctxt, + prop, mutator, valueType, keyDeser, deser, typeDeser); + } /** From adb5d1e683b9aee3b3c54bca08701a4416fbc65e Mon Sep 17 00:00:00 2001 From: "Kim, Joo Hyuk" Date: Sat, 1 Jun 2024 16:40:37 +0900 Subject: [PATCH 05/24] refactor: fix indents --- .../jackson/databind/deser/BasicDeserializerFactory.java | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/main/java/com/fasterxml/jackson/databind/deser/BasicDeserializerFactory.java b/src/main/java/com/fasterxml/jackson/databind/deser/BasicDeserializerFactory.java index ba3df26ba5..69f51fc2ef 100644 --- a/src/main/java/com/fasterxml/jackson/databind/deser/BasicDeserializerFactory.java +++ b/src/main/java/com/fasterxml/jackson/databind/deser/BasicDeserializerFactory.java @@ -528,8 +528,8 @@ private void _addSelectedPropertiesBasedCreator(DeserializationContext ctxt, if (unwrapper != null) { _reportUnwrappedCreatorProperty(ctxt, beanDesc, param); } - if ((name == null) && (injectId == null)) { - // Must be injectable or have name; without either won't work + // Must be injectable or have name; without either won't work + if ((name == null) && (injectId == null)) { ctxt.reportBadTypeDefinition(beanDesc, "Argument #%d of Creator %s has no property name (and is not Injectable): can not use as property-based Creator", i, candidate); From fb36d8ed6e0e16475c6305b2c1b556f01d8a12e1 Mon Sep 17 00:00:00 2001 From: "Kim, Joo Hyuk" Date: Sat, 1 Jun 2024 16:57:41 +0900 Subject: [PATCH 06/24] feature: Add index field to SettableAnyProperty so PropertyValueBuffer can just pour into the any-setter --- .../deser/BeanDeserializerFactory.java | 2 +- .../databind/deser/SettableAnyProperty.java | 47 +++++++++++-------- .../deser/impl/PropertyValueBuffer.java | 28 +++++------ 3 files changed, 41 insertions(+), 36 deletions(-) diff --git a/src/main/java/com/fasterxml/jackson/databind/deser/BeanDeserializerFactory.java b/src/main/java/com/fasterxml/jackson/databind/deser/BeanDeserializerFactory.java index ca3035e940..d7c310a153 100644 --- a/src/main/java/com/fasterxml/jackson/databind/deser/BeanDeserializerFactory.java +++ b/src/main/java/com/fasterxml/jackson/databind/deser/BeanDeserializerFactory.java @@ -958,7 +958,7 @@ protected SettableAnyProperty constructCreatorPropertyAnySetter(DeserializationC TypeDeserializer typeDeser = valueType.getTypeHandler(); // [databind#562] Allow @JsonAnySetter in creators return SettableAnyProperty.constructForMapParameter(ctxt, - prop, mutator, valueType, keyDeser, deser, typeDeser); + prop, mutator, valueType, keyDeser, deser, typeDeser, af.getIndex()); } diff --git a/src/main/java/com/fasterxml/jackson/databind/deser/SettableAnyProperty.java b/src/main/java/com/fasterxml/jackson/databind/deser/SettableAnyProperty.java index 249ab2e2b3..657fca7678 100644 --- a/src/main/java/com/fasterxml/jackson/databind/deser/SettableAnyProperty.java +++ b/src/main/java/com/fasterxml/jackson/databind/deser/SettableAnyProperty.java @@ -118,15 +118,13 @@ public static SettableAnyProperty constructForJsonNodeField(DeserializationConte ctxt.getNodeFactory()); } - /** + /** * @since 2.18 */ public static SettableAnyProperty constructForMapParameter(DeserializationContext ctxt, - BeanProperty property, - AnnotatedMember field, JavaType valueType, - KeyDeserializer keyDeser, - JsonDeserializer valueDeser, TypeDeserializer typeDeser) - { + BeanProperty property, AnnotatedMember field, JavaType valueType, KeyDeserializer keyDeser, + JsonDeserializer valueDeser, TypeDeserializer typeDeser, int parameterIndex + ) { Class mapType = field.getRawType(); // 02-Aug-2022, tatu: Ideally would be resolved to a concrete type by caller but // alas doesn't appear to happen. Nor does `BasicDeserializerFactory` expose method @@ -135,9 +133,7 @@ public static SettableAnyProperty constructForMapParameter(DeserializationContex mapType = LinkedHashMap.class; } ValueInstantiator vi = JDKValueInstantiators.findStdValueInstantiator(ctxt.getConfig(), mapType); - return new MapParamAnyProperty(property, field, valueType, - keyDeser, valueDeser, typeDeser, - vi); + return new MapParamAnyProperty(property, field, valueType, keyDeser, valueDeser, typeDeser, vi, parameterIndex); } @@ -183,6 +179,14 @@ Object readResolve() { */ public String getPropertyName() { return _property.getName(); } + /** + * Accessor for parameterIndex. + * @return -1 if not a parameterized setter, otherwise index of parameter + * + * @since 2.18 + */ + public int getParameterIndex() { return -1; } + /* /********************************************************** /* Public API, deserialization @@ -475,23 +479,22 @@ protected static class MapParamAnyProperty extends SettableAnyProperty protected final ValueInstantiator _valueInstantiator; - public MapParamAnyProperty(BeanProperty property, - AnnotatedMember field, JavaType valueType, - KeyDeserializer keyDeser, - JsonDeserializer valueDeser, TypeDeserializer typeDeser, - ValueInstantiator inst) - { - super(property, field, valueType, - keyDeser, valueDeser, typeDeser); + protected final int _parameterIndex; + + public MapParamAnyProperty(BeanProperty property, AnnotatedMember field, JavaType valueType, + KeyDeserializer keyDeser, JsonDeserializer valueDeser, TypeDeserializer typeDeser, + ValueInstantiator inst, int parameterIndex + ) { + super(property, field, valueType, keyDeser, valueDeser, typeDeser); _valueInstantiator = Objects.requireNonNull(inst, "ValueInstantiator for MapParameterAnyProperty cannot be `null`"); + _parameterIndex = parameterIndex; } @Override public SettableAnyProperty withValueDeserializer(JsonDeserializer deser) { - return new MapParamAnyProperty(_property, _setter, _type, - _keyDeserializer, deser, _valueTypeDeserializer, - _valueInstantiator); + return new MapParamAnyProperty(_property, _setter, _type, _keyDeserializer, deser, + _valueTypeDeserializer, _valueInstantiator, _parameterIndex); } @Override @@ -500,6 +503,10 @@ protected void _set(Object instance, Object propName, Object value) ((Map) instance).put(propName, value); } + @Override + public int getParameterIndex() { + return _parameterIndex; + } } } diff --git a/src/main/java/com/fasterxml/jackson/databind/deser/impl/PropertyValueBuffer.java b/src/main/java/com/fasterxml/jackson/databind/deser/impl/PropertyValueBuffer.java index c1b45db8e7..20c0d38c74 100644 --- a/src/main/java/com/fasterxml/jackson/databind/deser/impl/PropertyValueBuffer.java +++ b/src/main/java/com/fasterxml/jackson/databind/deser/impl/PropertyValueBuffer.java @@ -85,7 +85,10 @@ public class PropertyValueBuffer protected final SettableAnyProperty _anySetter; /** - * Flag that indicates whether this buffer has been consumed. + * Flag that indicates whether this buffer has been consumed, meaning + * that all properties have been read and assigned to the creator. + * + * @since 2.18 */ protected boolean _consumed; @@ -99,7 +102,7 @@ public class PropertyValueBuffer * @since 2.18 */ public PropertyValueBuffer(JsonParser p, DeserializationContext ctxt, int paramCount, - ObjectIdReader oir, SettableAnyProperty anySetter) + ObjectIdReader oir, SettableAnyProperty creatorAnySetter) { _parser = p; _context = ctxt; @@ -111,7 +114,7 @@ public PropertyValueBuffer(JsonParser p, DeserializationContext ctxt, int paramC } else { _paramsSeenBig = new BitSet(); } - _anySetter = anySetter; + _anySetter = creatorAnySetter; } @Deprecated // since 2.18 @@ -204,19 +207,14 @@ public Object[] getParameters(SettableBeanProperty[] props) } // [databind#562] since 2.18 : Respect @JsonAnySetter in @JsonCreator - if (_anySetter != null) { - for (int i = 0; i < _creatorParameters.length; i++) { - if (_creatorParameters[i] == null) { - // anySetter should know about index - Map anySetterMap = new HashMap<>(); - for (PropertyValue pv = this.buffered(); pv != null; pv = pv.next) { - pv.assign(anySetterMap); - } - _creatorParameters[i] = anySetterMap; - _consumed = true; - break; - } + if ((_anySetter != null) && (_anySetter.getParameterIndex() >= 0)) { + Map anySetterMap = new HashMap<>(); + for (PropertyValue pv = this.buffered(); pv != null; pv = pv.next) { + pv.assign(anySetterMap); } + _creatorParameters[_anySetter.getParameterIndex()] = anySetterMap; + // mark as consumed, so current PropertyValueBuffer does not get reused. + _consumed = true; } return _creatorParameters; } From 40aaf8820fd6207a5691fea455369ba1fe4cfe1d Mon Sep 17 00:00:00 2001 From: "Kim, Joo Hyuk" Date: Sat, 1 Jun 2024 17:09:14 +0900 Subject: [PATCH 07/24] test : Add record tests --- .../RecordCreatorWithAnySetter562Test.java | 54 +++++++++++++++++++ 1 file changed, 54 insertions(+) create mode 100644 src/test-jdk17/java/com/fasterxml/jackson/databind/records/RecordCreatorWithAnySetter562Test.java diff --git a/src/test-jdk17/java/com/fasterxml/jackson/databind/records/RecordCreatorWithAnySetter562Test.java b/src/test-jdk17/java/com/fasterxml/jackson/databind/records/RecordCreatorWithAnySetter562Test.java new file mode 100644 index 0000000000..895a1c1db6 --- /dev/null +++ b/src/test-jdk17/java/com/fasterxml/jackson/databind/records/RecordCreatorWithAnySetter562Test.java @@ -0,0 +1,54 @@ +package com.fasterxml.jackson.databind.records; + +import com.fasterxml.jackson.annotation.JsonAnySetter; +import com.fasterxml.jackson.annotation.JsonCreator; +import com.fasterxml.jackson.annotation.JsonProperty; +import com.fasterxml.jackson.databind.ObjectMapper; +import com.fasterxml.jackson.databind.testutil.DatabindTestUtil; +import org.junit.jupiter.api.Test; + +import java.util.Map; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.fail; + +public class RecordCreatorWithAnySetter562Test extends DatabindTestUtil +{ + + record RecordWithAnySetterCtor(int id, Map additionalProperties) { + @JsonCreator + public RecordWithAnySetterCtor(@JsonProperty("regular") int id, + @JsonAnySetter Map additionalProperties + ) { + this.id = id; + this.additionalProperties = additionalProperties; + } + } + + /* + /********************************************************************** + /* Test methods, alternate constructors + /********************************************************************** + */ + + private final ObjectMapper MAPPER = newJsonMapper(); + + @Test + public void testRecordWithAnySetterCtor() throws Exception + { + // First, only regular property mapped + RecordWithAnySetterCtor result = MAPPER.readValue(a2q("{'regular':13}"), + RecordWithAnySetterCtor.class); + assertEquals(13, result.id); + assertEquals(0, result.additionalProperties.size()); + + // Then with unknown properties + result = MAPPER.readValue(a2q("{'regular':13, 'unknown':99, 'extra':-1}"), + RecordWithAnySetterCtor.class); + assertEquals(13, result.id); + assertEquals(Integer.valueOf(99), result.additionalProperties.get("unknown")); + assertEquals(Integer.valueOf(-1), result.additionalProperties.get("extra")); + assertEquals(2, result.additionalProperties.size()); + } + +} From c66ca735cd83721679e738582253fda88f8497bf Mon Sep 17 00:00:00 2001 From: "Kim, Joo Hyuk" Date: Sat, 1 Jun 2024 21:34:28 +0900 Subject: [PATCH 08/24] Add support for ObjectNode --- .../deser/BeanDeserializerFactory.java | 51 +++++++++----- .../databind/deser/SettableAnyProperty.java | 70 +++++++++++++++++++ .../deser/impl/PropertyValueBuffer.java | 8 +-- .../creators/AnySetterForCreator562Test.java | 35 +++++++--- 4 files changed, 134 insertions(+), 30 deletions(-) diff --git a/src/main/java/com/fasterxml/jackson/databind/deser/BeanDeserializerFactory.java b/src/main/java/com/fasterxml/jackson/databind/deser/BeanDeserializerFactory.java index d7c310a153..18ef4a69d3 100644 --- a/src/main/java/com/fasterxml/jackson/databind/deser/BeanDeserializerFactory.java +++ b/src/main/java/com/fasterxml/jackson/databind/deser/BeanDeserializerFactory.java @@ -914,24 +914,43 @@ protected SettableAnyProperty constructCreatorPropertyAnySetter(DeserializationC BeanDescription beanDesc, AnnotatedMember mutator) throws JsonMappingException { - if (!(mutator instanceof AnnotatedParameter)){ - ctxt.reportBadDefinition(beanDesc.getType(), String.format( + JavaType keyType; + JavaType valueType; + BeanProperty prop; + int creatorIndex = -1; + + if (mutator instanceof AnnotatedParameter){ + AnnotatedParameter af = (AnnotatedParameter) mutator; + // get the type from the content type of the map object + JavaType fieldType = af.getType(); + if (fieldType.hasRawClass(JsonNode.class) || fieldType.hasRawClass(ObjectNode.class)) { + fieldType = resolveMemberAndTypeAnnotations(ctxt, mutator, fieldType); + // Deserialize is individual values of ObjectNode, not full ObjectNode, so: + valueType = ctxt.constructType(JsonNode.class); + prop = new BeanProperty.Std(PropertyName.construct(mutator.getName()), + fieldType, null, mutator, PropertyMetadata.STD_OPTIONAL); + + // Unlike with more complicated types, here we do not allow any annotation + // overrides etc but instead short-cut handling: + return SettableAnyProperty.constructForJsonNodeParameter(ctxt, prop, mutator, valueType, + ctxt.findRootValueDeserializer(valueType), af.getIndex()); + } else if (fieldType.isMapLikeType()) { + fieldType = resolveMemberAndTypeAnnotations(ctxt, mutator, fieldType); + keyType = fieldType.getKeyType(); + valueType = fieldType.getContentType(); + prop = new BeanProperty.Std(PropertyName.construct(mutator.getName()), + fieldType, null, mutator, PropertyMetadata.STD_OPTIONAL); + } else { + return ctxt.reportBadDefinition(beanDesc.getType(), String.format( + "Unsupported type for any-setter: %s -- only support `Map`s, `JsonNode` and `ObjectNode` ", + ClassUtil.getTypeDescription(fieldType))); + } + creatorIndex = af.getIndex(); + } else { + return ctxt.reportBadDefinition(beanDesc.getType(), String.format( "Unrecognized mutator type for any-setter: %s", ClassUtil.nameOf(mutator.getClass()))); } - AnnotatedParameter af = (AnnotatedParameter) mutator; - // get the type from the content type of the map object - JavaType fieldType = af.getType(); - if (!fieldType.isMapLikeType()) { - return ctxt.reportBadDefinition(beanDesc.getType(), String.format( - "Unsupported type for any-setter: %s -- only support `Map`s, `JsonNode` and `ObjectNode` ", - ClassUtil.getTypeDescription(fieldType))); - } - fieldType = resolveMemberAndTypeAnnotations(ctxt, mutator, fieldType); - JavaType keyType = fieldType.getKeyType(); - JavaType valueType = fieldType.getContentType(); - BeanProperty prop = new BeanProperty.Std(PropertyName.construct(mutator.getName()), - fieldType, null, mutator, PropertyMetadata.STD_OPTIONAL); // First: see if there are explicitly specified // and then possible direct deserializer override on accessor @@ -958,7 +977,7 @@ protected SettableAnyProperty constructCreatorPropertyAnySetter(DeserializationC TypeDeserializer typeDeser = valueType.getTypeHandler(); // [databind#562] Allow @JsonAnySetter in creators return SettableAnyProperty.constructForMapParameter(ctxt, - prop, mutator, valueType, keyDeser, deser, typeDeser, af.getIndex()); + prop, mutator, valueType, keyDeser, deser, typeDeser, creatorIndex); } diff --git a/src/main/java/com/fasterxml/jackson/databind/deser/SettableAnyProperty.java b/src/main/java/com/fasterxml/jackson/databind/deser/SettableAnyProperty.java index 657fca7678..af31e0a8a0 100644 --- a/src/main/java/com/fasterxml/jackson/databind/deser/SettableAnyProperty.java +++ b/src/main/java/com/fasterxml/jackson/databind/deser/SettableAnyProperty.java @@ -1,6 +1,7 @@ package com.fasterxml.jackson.databind.deser; import java.io.IOException; +import java.util.HashMap; import java.util.LinkedHashMap; import java.util.Map; import java.util.Objects; @@ -136,6 +137,12 @@ public static SettableAnyProperty constructForMapParameter(DeserializationContex return new MapParamAnyProperty(property, field, valueType, keyDeser, valueDeser, typeDeser, vi, parameterIndex); } + public static SettableAnyProperty constructForJsonNodeParameter(DeserializationContext ctxt, BeanProperty prop, + AnnotatedMember mutator, JavaType valueType, JsonDeserializer valueDeser, int parameterIndex + ) { + return new JsonNodeParameterAnyProperty(prop, mutator, valueType, valueDeser, ctxt.getNodeFactory(), parameterIndex); + } + // Abstract @since 2.14 public abstract SettableAnyProperty withValueDeserializer(JsonDeserializer deser); @@ -187,6 +194,15 @@ Object readResolve() { */ public int getParameterIndex() { return -1; } + /** + * Create an instance of value to set any properties. + * + * @since 2.18 + */ + public Object createParameterObject() { + throw new UnsupportedOperationException("Cannot call createProperty() on "+getClass().getName()); + } + /* /********************************************************** /* Public API, deserialization @@ -507,6 +523,60 @@ protected void _set(Object instance, Object propName, Object value) public int getParameterIndex() { return _parameterIndex; } + + @Override + public Object createParameterObject() { + return new HashMap<>(); + } + } + + /** + * @since 2.18 + */ + protected static class JsonNodeParameterAnyProperty extends SettableAnyProperty + implements java.io.Serializable + { + private static final long serialVersionUID = 1L; + + protected final JsonNodeFactory _nodeFactory; + + protected final int _parameterIndex; + + public JsonNodeParameterAnyProperty(BeanProperty property, AnnotatedMember field, JavaType valueType, + JsonDeserializer valueDeser, JsonNodeFactory nodeFactory, int parameterIndex) + { + super(property, field, valueType, null, valueDeser, null); + _nodeFactory = nodeFactory; + _parameterIndex = parameterIndex; + } + + + // Let's override since this is much simpler with JsonNodes + @Override + public Object deserialize(JsonParser p, DeserializationContext ctxt) + throws IOException + { + return _valueDeserializer.deserialize(p, ctxt); + } + + @Override + protected void _set(Object instance, Object propName, Object value) + throws Exception + { + ObjectNode objectNode = (ObjectNode) instance; + objectNode.set((String) propName, (JsonNode) value); + } + + // Should not get called but... + @Override + public SettableAnyProperty withValueDeserializer(JsonDeserializer deser) { return this; } + + @Override + public int getParameterIndex() { return _parameterIndex; } + + @Override + public Object createParameterObject() { return _nodeFactory.objectNode(); } + } } diff --git a/src/main/java/com/fasterxml/jackson/databind/deser/impl/PropertyValueBuffer.java b/src/main/java/com/fasterxml/jackson/databind/deser/impl/PropertyValueBuffer.java index 20c0d38c74..b49936fa95 100644 --- a/src/main/java/com/fasterxml/jackson/databind/deser/impl/PropertyValueBuffer.java +++ b/src/main/java/com/fasterxml/jackson/databind/deser/impl/PropertyValueBuffer.java @@ -2,8 +2,6 @@ import java.io.IOException; import java.util.BitSet; -import java.util.HashMap; -import java.util.Map; import com.fasterxml.jackson.core.JsonParser; @@ -208,11 +206,11 @@ public Object[] getParameters(SettableBeanProperty[] props) // [databind#562] since 2.18 : Respect @JsonAnySetter in @JsonCreator if ((_anySetter != null) && (_anySetter.getParameterIndex() >= 0)) { - Map anySetterMap = new HashMap<>(); + Object anySetterParameterObject = _anySetter.createParameterObject(); for (PropertyValue pv = this.buffered(); pv != null; pv = pv.next) { - pv.assign(anySetterMap); + pv.assign(anySetterParameterObject); } - _creatorParameters[_anySetter.getParameterIndex()] = anySetterMap; + _creatorParameters[_anySetter.getParameterIndex()] = anySetterParameterObject; // mark as consumed, so current PropertyValueBuffer does not get reused. _consumed = true; } diff --git a/src/test/java/com/fasterxml/jackson/databind/deser/creators/AnySetterForCreator562Test.java b/src/test/java/com/fasterxml/jackson/databind/deser/creators/AnySetterForCreator562Test.java index aef766f9ca..4145995d96 100644 --- a/src/test/java/com/fasterxml/jackson/databind/deser/creators/AnySetterForCreator562Test.java +++ b/src/test/java/com/fasterxml/jackson/databind/deser/creators/AnySetterForCreator562Test.java @@ -7,6 +7,7 @@ import com.fasterxml.jackson.annotation.JsonCreator; import com.fasterxml.jackson.annotation.JsonProperty; import com.fasterxml.jackson.databind.DeserializationFeature; +import com.fasterxml.jackson.databind.JsonNode; import com.fasterxml.jackson.databind.ObjectMapper; import com.fasterxml.jackson.databind.exc.InvalidDefinitionException; import com.fasterxml.jackson.databind.exc.MismatchedInputException; @@ -19,11 +20,9 @@ public class AnySetterForCreator562Test extends DatabindTestUtil { - // [databind#562] static class POJO562 { String a; - Map stuff; @JsonCreator @@ -35,7 +34,20 @@ public POJO562(@JsonProperty("a") String a, } } - // [databind#562]: failing case + static class PojoWithNodeAnySetter + { + String a; + JsonNode anySetterNode; + + @JsonCreator + public PojoWithNodeAnySetter(@JsonProperty("a") String a, + @JsonAnySetter JsonNode leftovers + ) { + this.a = a; + anySetterNode = leftovers; + } + } + static class MultipleAny562 { @JsonCreator @@ -46,11 +58,9 @@ public MultipleAny562(@JsonProperty("a") String a, } } - // [databind#562] static class PojoWithDisabled { String a; - Map stuff; @JsonCreator @@ -64,7 +74,6 @@ public PojoWithDisabled(@JsonProperty("a") String a, private final ObjectMapper MAPPER = newJsonMapper(); - // [databind#562] @Test public void anySetterViaCreator562() throws Exception { @@ -81,7 +90,17 @@ public void anySetterViaCreator562() throws Exception assertEquals(expected, pojo.stuff); } - // [databind#562] + @Test + public void testNodeAnySetterViaCreator562() throws Exception + { + PojoWithNodeAnySetter pojo = MAPPER.readValue( + a2q("{'a':'value', 'b':42, 'c': 111}"), + PojoWithNodeAnySetter.class); + + assertEquals("value", pojo.a); + assertEquals(a2q("{'c':111,'b':42}"), pojo.anySetterNode + ""); + } + @Test public void testWithFailureConfigs562() throws Exception { @@ -116,7 +135,6 @@ public void testWithFailureConfigs562() throws Exception } } - // [databind#562] @Test public void testAnySetterViaCreator562FailForDup() throws Exception { @@ -129,7 +147,6 @@ public void testAnySetterViaCreator562FailForDup() throws Exception } } - // [databind#562] @Test public void testAnySetterViaCreator562Disabled() throws Exception { From 326e491f9656fbcfd259c01d5762868d23c1e4c6 Mon Sep 17 00:00:00 2001 From: "Kim, Joo Hyuk" Date: Sat, 1 Jun 2024 21:58:42 +0900 Subject: [PATCH 09/24] Clean up rest of the code --- .../deser/BeanDeserializerFactory.java | 5 +- .../databind/deser/SettableAnyProperty.java | 3 +- .../deser/impl/PropertyBasedCreator.java | 3 +- .../failing/AnySetterForCreator562Test.java | 48 ------------------- 4 files changed, 6 insertions(+), 53 deletions(-) delete mode 100644 src/test/java/com/fasterxml/jackson/failing/AnySetterForCreator562Test.java diff --git a/src/main/java/com/fasterxml/jackson/databind/deser/BeanDeserializerFactory.java b/src/main/java/com/fasterxml/jackson/databind/deser/BeanDeserializerFactory.java index 18ef4a69d3..f074e11841 100644 --- a/src/main/java/com/fasterxml/jackson/databind/deser/BeanDeserializerFactory.java +++ b/src/main/java/com/fasterxml/jackson/databind/deser/BeanDeserializerFactory.java @@ -662,8 +662,9 @@ protected void addBeanProps(DeserializationContext ctxt, } // since 2.18 - private SettableAnyProperty _resolveAnySetter(DeserializationContext ctxt, BeanDescription beanDesc, - SettableBeanProperty[] creatorProps) throws JsonMappingException + private SettableAnyProperty _resolveAnySetter(DeserializationContext ctxt, + BeanDescription beanDesc, SettableBeanProperty[] creatorProps) + throws JsonMappingException { // Find the regular method/field level any-setter AnnotatedMember anySetter = beanDesc.findAnySetterAccessor(); diff --git a/src/main/java/com/fasterxml/jackson/databind/deser/SettableAnyProperty.java b/src/main/java/com/fasterxml/jackson/databind/deser/SettableAnyProperty.java index af31e0a8a0..5b8a3c0a74 100644 --- a/src/main/java/com/fasterxml/jackson/databind/deser/SettableAnyProperty.java +++ b/src/main/java/com/fasterxml/jackson/databind/deser/SettableAnyProperty.java @@ -200,7 +200,7 @@ Object readResolve() { * @since 2.18 */ public Object createParameterObject() { - throw new UnsupportedOperationException("Cannot call createProperty() on "+getClass().getName()); + throw new UnsupportedOperationException("Cannot call createParameterObject() on " + getClass().getName()); } /* @@ -550,7 +550,6 @@ public JsonNodeParameterAnyProperty(BeanProperty property, AnnotatedMember field _parameterIndex = parameterIndex; } - // Let's override since this is much simpler with JsonNodes @Override public Object deserialize(JsonParser p, DeserializationContext ctxt) diff --git a/src/main/java/com/fasterxml/jackson/databind/deser/impl/PropertyBasedCreator.java b/src/main/java/com/fasterxml/jackson/databind/deser/impl/PropertyBasedCreator.java index b2fd6e7e9a..b11d2111b5 100644 --- a/src/main/java/com/fasterxml/jackson/databind/deser/impl/PropertyBasedCreator.java +++ b/src/main/java/com/fasterxml/jackson/databind/deser/impl/PropertyBasedCreator.java @@ -204,7 +204,8 @@ public PropertyValueBuffer startBuilding(JsonParser p, DeserializationContext ct * @since 2.18 (added SettableAnyProperty parameter) */ public PropertyValueBuffer startBuildingWithAnySetter(JsonParser p, DeserializationContext ctxt, - ObjectIdReader oir, SettableAnyProperty anySetter) { + ObjectIdReader oir, SettableAnyProperty anySetter + ) { return new PropertyValueBuffer(p, ctxt, _propertyCount, oir, anySetter); } diff --git a/src/test/java/com/fasterxml/jackson/failing/AnySetterForCreator562Test.java b/src/test/java/com/fasterxml/jackson/failing/AnySetterForCreator562Test.java deleted file mode 100644 index eca7692a78..0000000000 --- a/src/test/java/com/fasterxml/jackson/failing/AnySetterForCreator562Test.java +++ /dev/null @@ -1,48 +0,0 @@ -package com.fasterxml.jackson.failing; - -import java.util.Collections; -import java.util.Map; - -import org.junit.jupiter.api.Test; - -import com.fasterxml.jackson.annotation.JsonAnySetter; -import com.fasterxml.jackson.annotation.JsonCreator; -import com.fasterxml.jackson.annotation.JsonProperty; -import com.fasterxml.jackson.databind.ObjectMapper; -import com.fasterxml.jackson.databind.testutil.DatabindTestUtil; - -import static org.junit.jupiter.api.Assertions.assertEquals; - -class AnySetterForCreator562Test extends DatabindTestUtil -{ - // [databind#562] - static class POJO562 - { - String a; - - Map stuff; - - @JsonCreator - public POJO562(@JsonProperty("a") String a, - @JsonAnySetter Map - leftovers) { - this.a = a; - stuff = leftovers; - } - } - - private final ObjectMapper MAPPER = newJsonMapper(); - - // [databind#562] - @Test - void anySetterViaCreator562() throws Exception - { - POJO562 pojo = MAPPER.readValue(a2q( - "{'a':'value', 'b':42}" - ), - POJO562.class); - assertEquals("value", pojo.a); - assertEquals(Collections.singletonMap("b", Integer.valueOf(42)), - pojo.stuff); - } -} From bf91015259409583dd0ceff2fcd022cbc21340d1 Mon Sep 17 00:00:00 2001 From: "Kim, Joo Hyuk" Date: Sat, 1 Jun 2024 22:16:36 +0900 Subject: [PATCH 10/24] Refactor : simplify implementations --- .../deser/BeanDeserializerFactory.java | 70 ++++--------------- .../databind/deser/impl/PropertyValue.java | 27 ------- .../deser/impl/PropertyValueBuffer.java | 8 +-- .../RecordCreatorWithAnySetter562Test.java | 8 ++- .../creators/AnySetterForCreator562Test.java | 1 + 5 files changed, 25 insertions(+), 89 deletions(-) diff --git a/src/main/java/com/fasterxml/jackson/databind/deser/BeanDeserializerFactory.java b/src/main/java/com/fasterxml/jackson/databind/deser/BeanDeserializerFactory.java index f074e11841..06348b999b 100644 --- a/src/main/java/com/fasterxml/jackson/databind/deser/BeanDeserializerFactory.java +++ b/src/main/java/com/fasterxml/jackson/databind/deser/BeanDeserializerFactory.java @@ -676,7 +676,7 @@ private SettableAnyProperty _resolveAnySetter(DeserializationContext ctxt, for (SettableBeanProperty prop : creatorProps) { AnnotatedMember member = prop.getMember(); if (member != null && Boolean.TRUE.equals(ctxt.getAnnotationIntrospector().hasAnySetter(member))) { - return constructCreatorPropertyAnySetter(ctxt, beanDesc, member); + return constructAnySetter(ctxt, beanDesc, member); } } } @@ -831,6 +831,9 @@ protected SettableAnyProperty constructAnySetter(DeserializationContext ctxt, JavaType keyType; JavaType valueType; final boolean isField = mutator instanceof AnnotatedField; + // [databind#562] Allow @JsonAnySetter on Creator constructor + final boolean isParameter = mutator instanceof AnnotatedParameter; + int creatorIndex = -1; if (mutator instanceof AnnotatedMethod) { // we know it's a 2-arg method, second arg is the value @@ -871,56 +874,7 @@ protected SettableAnyProperty constructAnySetter(DeserializationContext ctxt, "Unsupported type for any-setter: %s -- only support `Map`s, `JsonNode` and `ObjectNode` ", ClassUtil.getTypeDescription(fieldType))); } - } else { - return ctxt.reportBadDefinition(beanDesc.getType(), String.format( - "Unrecognized mutator type for any-setter: %s", - ClassUtil.nameOf(mutator.getClass()))); - } - // First: see if there are explicitly specified - // and then possible direct deserializer override on accessor - KeyDeserializer keyDeser = findKeyDeserializerFromAnnotation(ctxt, mutator); - if (keyDeser == null) { - keyDeser = keyType.getValueHandler(); - } - if (keyDeser == null) { - keyDeser = ctxt.findKeyDeserializer(keyType, prop); - } else { - if (keyDeser instanceof ContextualKeyDeserializer) { - keyDeser = ((ContextualKeyDeserializer) keyDeser) - .createContextual(ctxt, prop); - } - } - JsonDeserializer deser = findContentDeserializerFromAnnotation(ctxt, mutator); - if (deser == null) { - deser = valueType.getValueHandler(); - } - if (deser != null) { - // As per [databind#462] need to ensure we contextualize deserializer before passing it on - deser = (JsonDeserializer) ctxt.handlePrimaryContextualization(deser, prop, valueType); - } - TypeDeserializer typeDeser = valueType.getTypeHandler(); - if (isField) { - return SettableAnyProperty.constructForMapField(ctxt, - prop, mutator, valueType, keyDeser, deser, typeDeser); - } - return SettableAnyProperty.constructForMethod(ctxt, - prop, mutator, valueType, keyDeser, deser, typeDeser); - } - - /** - * @param mutator Instance of {@link AnnotatedParameter} that should represent any-setter parameter. - */ - @SuppressWarnings("unchecked") - protected SettableAnyProperty constructCreatorPropertyAnySetter(DeserializationContext ctxt, - BeanDescription beanDesc, AnnotatedMember mutator) - throws JsonMappingException - { - JavaType keyType; - JavaType valueType; - BeanProperty prop; - int creatorIndex = -1; - - if (mutator instanceof AnnotatedParameter){ + } else if (isParameter) { AnnotatedParameter af = (AnnotatedParameter) mutator; // get the type from the content type of the map object JavaType fieldType = af.getType(); @@ -976,12 +930,18 @@ protected SettableAnyProperty constructCreatorPropertyAnySetter(DeserializationC deser = (JsonDeserializer) ctxt.handlePrimaryContextualization(deser, prop, valueType); } TypeDeserializer typeDeser = valueType.getTypeHandler(); - // [databind#562] Allow @JsonAnySetter in creators - return SettableAnyProperty.constructForMapParameter(ctxt, - prop, mutator, valueType, keyDeser, deser, typeDeser, creatorIndex); + if (isField) { + return SettableAnyProperty.constructForMapField(ctxt, + prop, mutator, valueType, keyDeser, deser, typeDeser); + } + if (isParameter) { + return SettableAnyProperty.constructForMapParameter(ctxt, + prop, mutator, valueType, keyDeser, deser, typeDeser, creatorIndex); + } + return SettableAnyProperty.constructForMethod(ctxt, + prop, mutator, valueType, keyDeser, deser, typeDeser); } - /** * Method that will construct a regular bean property setter using * the given setter method. diff --git a/src/main/java/com/fasterxml/jackson/databind/deser/impl/PropertyValue.java b/src/main/java/com/fasterxml/jackson/databind/deser/impl/PropertyValue.java index 34b4de775e..6558b3dcf3 100644 --- a/src/main/java/com/fasterxml/jackson/databind/deser/impl/PropertyValue.java +++ b/src/main/java/com/fasterxml/jackson/databind/deser/impl/PropertyValue.java @@ -24,10 +24,6 @@ protected PropertyValue(PropertyValue next, Object value) this.value = value; } - public static PropertyValue empty() { - return Empty.INSTANCE; - } - /** * Method called to assign stored value of this property to specified * bean instance @@ -117,27 +113,4 @@ public void assign(Object bean) ((java.util.Map) bean).put(_key, value); } } - - /** - * Property value type used do nothing - */ - private final static class Empty - extends PropertyValue - { - - public static final PropertyValue INSTANCE = new Empty(); - - private Empty() - { - super(null, null); - } - - @SuppressWarnings("unchecked") - @Override - public void assign(Object bean) - throws IOException - { - // no-op - } - } } diff --git a/src/main/java/com/fasterxml/jackson/databind/deser/impl/PropertyValueBuffer.java b/src/main/java/com/fasterxml/jackson/databind/deser/impl/PropertyValueBuffer.java index b49936fa95..42dce02e97 100644 --- a/src/main/java/com/fasterxml/jackson/databind/deser/impl/PropertyValueBuffer.java +++ b/src/main/java/com/fasterxml/jackson/databind/deser/impl/PropertyValueBuffer.java @@ -84,7 +84,7 @@ public class PropertyValueBuffer /** * Flag that indicates whether this buffer has been consumed, meaning - * that all properties have been read and assigned to the creator. + * that all properties have been read and assigned. * * @since 2.18 */ @@ -100,7 +100,7 @@ public class PropertyValueBuffer * @since 2.18 */ public PropertyValueBuffer(JsonParser p, DeserializationContext ctxt, int paramCount, - ObjectIdReader oir, SettableAnyProperty creatorAnySetter) + ObjectIdReader oir, SettableAnyProperty anySetter) { _parser = p; _context = ctxt; @@ -112,7 +112,7 @@ public PropertyValueBuffer(JsonParser p, DeserializationContext ctxt, int paramC } else { _paramsSeenBig = new BitSet(); } - _anySetter = creatorAnySetter; + _anySetter = anySetter; } @Deprecated // since 2.18 @@ -301,7 +301,7 @@ public Object handleIdValue(final DeserializationContext ctxt, Object bean) thro protected PropertyValue buffered() { if (_consumed) { - return PropertyValue.empty(); + return null; } return _buffered; } diff --git a/src/test-jdk17/java/com/fasterxml/jackson/databind/records/RecordCreatorWithAnySetter562Test.java b/src/test-jdk17/java/com/fasterxml/jackson/databind/records/RecordCreatorWithAnySetter562Test.java index 895a1c1db6..0ed048be1f 100644 --- a/src/test-jdk17/java/com/fasterxml/jackson/databind/records/RecordCreatorWithAnySetter562Test.java +++ b/src/test-jdk17/java/com/fasterxml/jackson/databind/records/RecordCreatorWithAnySetter562Test.java @@ -12,10 +12,12 @@ import static org.junit.jupiter.api.Assertions.assertEquals; import static org.junit.jupiter.api.Assertions.fail; -public class RecordCreatorWithAnySetter562Test extends DatabindTestUtil +// [databind#562] Allow @JsonAnySetter on Creator constructors +public class RecordCreatorWithAnySetter562Test + extends DatabindTestUtil { - - record RecordWithAnySetterCtor(int id, Map additionalProperties) { + record RecordWithAnySetterCtor(int id, + Map additionalProperties) { @JsonCreator public RecordWithAnySetterCtor(@JsonProperty("regular") int id, @JsonAnySetter Map additionalProperties diff --git a/src/test/java/com/fasterxml/jackson/databind/deser/creators/AnySetterForCreator562Test.java b/src/test/java/com/fasterxml/jackson/databind/deser/creators/AnySetterForCreator562Test.java index 4145995d96..6eb6089827 100644 --- a/src/test/java/com/fasterxml/jackson/databind/deser/creators/AnySetterForCreator562Test.java +++ b/src/test/java/com/fasterxml/jackson/databind/deser/creators/AnySetterForCreator562Test.java @@ -18,6 +18,7 @@ import static org.junit.jupiter.api.Assertions.assertEquals; import static org.junit.jupiter.api.Assertions.fail; +// [databind#562] Allow @JsonAnySetter on Creator constructors public class AnySetterForCreator562Test extends DatabindTestUtil { static class POJO562 From 7d40a43474c6f786528db63e45cb9a807c420cbc Mon Sep 17 00:00:00 2001 From: "Kim, Joo Hyuk" Date: Sat, 1 Jun 2024 22:26:16 +0900 Subject: [PATCH 11/24] Another clean up --- .../deser/BeanDeserializerFactory.java | 10 +++++----- .../databind/deser/SettableAnyProperty.java | 18 ++++++++---------- 2 files changed, 13 insertions(+), 15 deletions(-) diff --git a/src/main/java/com/fasterxml/jackson/databind/deser/BeanDeserializerFactory.java b/src/main/java/com/fasterxml/jackson/databind/deser/BeanDeserializerFactory.java index 06348b999b..0b26f14271 100644 --- a/src/main/java/com/fasterxml/jackson/databind/deser/BeanDeserializerFactory.java +++ b/src/main/java/com/fasterxml/jackson/databind/deser/BeanDeserializerFactory.java @@ -833,7 +833,7 @@ protected SettableAnyProperty constructAnySetter(DeserializationContext ctxt, final boolean isField = mutator instanceof AnnotatedField; // [databind#562] Allow @JsonAnySetter on Creator constructor final boolean isParameter = mutator instanceof AnnotatedParameter; - int creatorIndex = -1; + int parameterIndex = -1; if (mutator instanceof AnnotatedMethod) { // we know it's a 2-arg method, second arg is the value @@ -876,8 +876,9 @@ protected SettableAnyProperty constructAnySetter(DeserializationContext ctxt, } } else if (isParameter) { AnnotatedParameter af = (AnnotatedParameter) mutator; - // get the type from the content type of the map object JavaType fieldType = af.getType(); + parameterIndex = af.getIndex(); + if (fieldType.hasRawClass(JsonNode.class) || fieldType.hasRawClass(ObjectNode.class)) { fieldType = resolveMemberAndTypeAnnotations(ctxt, mutator, fieldType); // Deserialize is individual values of ObjectNode, not full ObjectNode, so: @@ -888,7 +889,7 @@ protected SettableAnyProperty constructAnySetter(DeserializationContext ctxt, // Unlike with more complicated types, here we do not allow any annotation // overrides etc but instead short-cut handling: return SettableAnyProperty.constructForJsonNodeParameter(ctxt, prop, mutator, valueType, - ctxt.findRootValueDeserializer(valueType), af.getIndex()); + ctxt.findRootValueDeserializer(valueType), parameterIndex); } else if (fieldType.isMapLikeType()) { fieldType = resolveMemberAndTypeAnnotations(ctxt, mutator, fieldType); keyType = fieldType.getKeyType(); @@ -900,7 +901,6 @@ protected SettableAnyProperty constructAnySetter(DeserializationContext ctxt, "Unsupported type for any-setter: %s -- only support `Map`s, `JsonNode` and `ObjectNode` ", ClassUtil.getTypeDescription(fieldType))); } - creatorIndex = af.getIndex(); } else { return ctxt.reportBadDefinition(beanDesc.getType(), String.format( "Unrecognized mutator type for any-setter: %s", @@ -936,7 +936,7 @@ protected SettableAnyProperty constructAnySetter(DeserializationContext ctxt, } if (isParameter) { return SettableAnyProperty.constructForMapParameter(ctxt, - prop, mutator, valueType, keyDeser, deser, typeDeser, creatorIndex); + prop, mutator, valueType, keyDeser, deser, typeDeser, parameterIndex); } return SettableAnyProperty.constructForMethod(ctxt, prop, mutator, valueType, keyDeser, deser, typeDeser); diff --git a/src/main/java/com/fasterxml/jackson/databind/deser/SettableAnyProperty.java b/src/main/java/com/fasterxml/jackson/databind/deser/SettableAnyProperty.java index 5b8a3c0a74..ae1c96e8f6 100644 --- a/src/main/java/com/fasterxml/jackson/databind/deser/SettableAnyProperty.java +++ b/src/main/java/com/fasterxml/jackson/databind/deser/SettableAnyProperty.java @@ -499,8 +499,8 @@ protected static class MapParamAnyProperty extends SettableAnyProperty public MapParamAnyProperty(BeanProperty property, AnnotatedMember field, JavaType valueType, KeyDeserializer keyDeser, JsonDeserializer valueDeser, TypeDeserializer typeDeser, - ValueInstantiator inst, int parameterIndex - ) { + ValueInstantiator inst, int parameterIndex) + { super(property, field, valueType, keyDeser, valueDeser, typeDeser); _valueInstantiator = Objects.requireNonNull(inst, "ValueInstantiator for MapParameterAnyProperty cannot be `null`"); _parameterIndex = parameterIndex; @@ -520,17 +520,16 @@ protected void _set(Object instance, Object propName, Object value) } @Override - public int getParameterIndex() { - return _parameterIndex; - } + public int getParameterIndex() { return _parameterIndex; } @Override - public Object createParameterObject() { - return new HashMap<>(); - } + public Object createParameterObject() { return new HashMap<>(); } + } /** + * [databind#562] Allow @JsonAnySetter on Creator constructor + * * @since 2.18 */ protected static class JsonNodeParameterAnyProperty extends SettableAnyProperty @@ -562,8 +561,7 @@ public Object deserialize(JsonParser p, DeserializationContext ctxt) protected void _set(Object instance, Object propName, Object value) throws Exception { - ObjectNode objectNode = (ObjectNode) instance; - objectNode.set((String) propName, (JsonNode) value); + ((ObjectNode) instance).set((String) propName, (JsonNode) value); } // Should not get called but... From b96725c1189cbf22cae406fa95acdf4f75b66a47 Mon Sep 17 00:00:00 2001 From: "Kim, Joo Hyuk" Date: Sat, 1 Jun 2024 22:30:48 +0900 Subject: [PATCH 12/24] Update SettableAnyProperty.java --- .../jackson/databind/deser/SettableAnyProperty.java | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/src/main/java/com/fasterxml/jackson/databind/deser/SettableAnyProperty.java b/src/main/java/com/fasterxml/jackson/databind/deser/SettableAnyProperty.java index ae1c96e8f6..081f258e49 100644 --- a/src/main/java/com/fasterxml/jackson/databind/deser/SettableAnyProperty.java +++ b/src/main/java/com/fasterxml/jackson/databind/deser/SettableAnyProperty.java @@ -134,7 +134,7 @@ public static SettableAnyProperty constructForMapParameter(DeserializationContex mapType = LinkedHashMap.class; } ValueInstantiator vi = JDKValueInstantiators.findStdValueInstantiator(ctxt.getConfig(), mapType); - return new MapParamAnyProperty(property, field, valueType, keyDeser, valueDeser, typeDeser, vi, parameterIndex); + return new MapParameterAnyProperty(property, field, valueType, keyDeser, valueDeser, typeDeser, vi, parameterIndex); } public static SettableAnyProperty constructForJsonNodeParameter(DeserializationContext ctxt, BeanProperty prop, @@ -488,7 +488,7 @@ public SettableAnyProperty withValueDeserializer(JsonDeserializer deser) * * @since 2.18 */ - protected static class MapParamAnyProperty extends SettableAnyProperty + protected static class MapParameterAnyProperty extends SettableAnyProperty implements java.io.Serializable { private static final long serialVersionUID = 1L; @@ -497,7 +497,7 @@ protected static class MapParamAnyProperty extends SettableAnyProperty protected final int _parameterIndex; - public MapParamAnyProperty(BeanProperty property, AnnotatedMember field, JavaType valueType, + public MapParameterAnyProperty(BeanProperty property, AnnotatedMember field, JavaType valueType, KeyDeserializer keyDeser, JsonDeserializer valueDeser, TypeDeserializer typeDeser, ValueInstantiator inst, int parameterIndex) { @@ -509,7 +509,7 @@ public MapParamAnyProperty(BeanProperty property, AnnotatedMember field, JavaTyp @Override public SettableAnyProperty withValueDeserializer(JsonDeserializer deser) { - return new MapParamAnyProperty(_property, _setter, _type, _keyDeserializer, deser, + return new MapParameterAnyProperty(_property, _setter, _type, _keyDeserializer, deser, _valueTypeDeserializer, _valueInstantiator, _parameterIndex); } From 0e11a15e3e2626011005d961e160333c764dcec5 Mon Sep 17 00:00:00 2001 From: "Kim, Joo Hyuk" Date: Sat, 8 Jun 2024 21:31:47 +0900 Subject: [PATCH 13/24] Revise comment and throw UnsupportedOperationException in SettableAnyProperty --- .../jackson/databind/deser/SettableAnyProperty.java | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/src/main/java/com/fasterxml/jackson/databind/deser/SettableAnyProperty.java b/src/main/java/com/fasterxml/jackson/databind/deser/SettableAnyProperty.java index 081f258e49..f165b1ebe3 100644 --- a/src/main/java/com/fasterxml/jackson/databind/deser/SettableAnyProperty.java +++ b/src/main/java/com/fasterxml/jackson/databind/deser/SettableAnyProperty.java @@ -195,7 +195,7 @@ Object readResolve() { public int getParameterIndex() { return -1; } /** - * Create an instance of value to set any properties. + * Create an instance of value to pass through Creator parameter. * * @since 2.18 */ @@ -566,7 +566,9 @@ protected void _set(Object instance, Object propName, Object value) // Should not get called but... @Override - public SettableAnyProperty withValueDeserializer(JsonDeserializer deser) { return this; } + public SettableAnyProperty withValueDeserializer(JsonDeserializer deser) { + throw new UnsupportedOperationException("Cannot call withValueDeserializer() on " + getClass().getName()); + } @Override public int getParameterIndex() { return _parameterIndex; } From d678cb4fe9947d493691522e7ae09b7af5fa5235 Mon Sep 17 00:00:00 2001 From: "Kim, Joo Hyuk" Date: Sat, 8 Jun 2024 22:52:13 +0900 Subject: [PATCH 14/24] Replace _consumedFlag, by actually using ProeprtyValue class --- .../databind/deser/BeanDeserializer.java | 2 +- .../databind/deser/impl/PropertyValue.java | 50 +++++++++++++++++++ .../deser/impl/PropertyValueBuffer.java | 23 +++------ 3 files changed, 57 insertions(+), 18 deletions(-) diff --git a/src/main/java/com/fasterxml/jackson/databind/deser/BeanDeserializer.java b/src/main/java/com/fasterxml/jackson/databind/deser/BeanDeserializer.java index 7656dc12b5..e83b11a26f 100644 --- a/src/main/java/com/fasterxml/jackson/databind/deser/BeanDeserializer.java +++ b/src/main/java/com/fasterxml/jackson/databind/deser/BeanDeserializer.java @@ -499,7 +499,7 @@ protected Object _deserializeUsingPropertyBased(final JsonParser p, final Deseri // "any property"? if (_anySetter != null) { try { - buffer.bufferAnyProperty(_anySetter, propName, _anySetter.deserialize(p, ctxt)); + buffer.bufferAnyParameterProperty(_anySetter, propName, _anySetter.deserialize(p, ctxt)); } catch (Exception e) { wrapAndThrow(e, _beanType.getRawClass(), propName, ctxt); } diff --git a/src/main/java/com/fasterxml/jackson/databind/deser/impl/PropertyValue.java b/src/main/java/com/fasterxml/jackson/databind/deser/impl/PropertyValue.java index 6558b3dcf3..4337f195fe 100644 --- a/src/main/java/com/fasterxml/jackson/databind/deser/impl/PropertyValue.java +++ b/src/main/java/com/fasterxml/jackson/databind/deser/impl/PropertyValue.java @@ -31,6 +31,18 @@ protected PropertyValue(PropertyValue next, Object value) public abstract void assign(Object bean) throws IOException; + /** + * Method called to assign stored value of this property to specified + * parameter object. + * + * @since 2.18 + */ + public void setValue(Object parameterObject) + throws IOException + { + throw new UnsupportedOperationException("Should not be called by this type " + getClass().getName()); + } + /* /********************************************************** /* Concrete property value classes @@ -113,4 +125,42 @@ public void assign(Object bean) ((java.util.Map) bean).put(_key, value); } } + + /** + * Property value type used when storing entries to be passed + * to constructor of POJO using "any-setter". + * + * @since 2.18 + */ + final static class AnyParameter + extends PropertyValue + { + final SettableAnyProperty _property; + final String _propertyName; + + public AnyParameter(PropertyValue next, Object value, + SettableAnyProperty prop, + String propName) + { + super(next, value); + _property = prop; + _propertyName = propName; + } + + @Override + public void assign(Object bean) + throws IOException + { + // do nothing, as we are not assigning to a bean + // instead, we are assigning to a parameter object via setValue field. + } + + @Override + public void setValue(Object parameterObject) + throws IOException + { + // AnyParameter + _property.set(parameterObject, _propertyName, value); + } + } } diff --git a/src/main/java/com/fasterxml/jackson/databind/deser/impl/PropertyValueBuffer.java b/src/main/java/com/fasterxml/jackson/databind/deser/impl/PropertyValueBuffer.java index 42dce02e97..749b752e4f 100644 --- a/src/main/java/com/fasterxml/jackson/databind/deser/impl/PropertyValueBuffer.java +++ b/src/main/java/com/fasterxml/jackson/databind/deser/impl/PropertyValueBuffer.java @@ -82,14 +82,6 @@ public class PropertyValueBuffer */ protected final SettableAnyProperty _anySetter; - /** - * Flag that indicates whether this buffer has been consumed, meaning - * that all properties have been read and assigned. - * - * @since 2.18 - */ - protected boolean _consumed; - /* /********************************************************** /* Life-cycle @@ -208,11 +200,9 @@ public Object[] getParameters(SettableBeanProperty[] props) if ((_anySetter != null) && (_anySetter.getParameterIndex() >= 0)) { Object anySetterParameterObject = _anySetter.createParameterObject(); for (PropertyValue pv = this.buffered(); pv != null; pv = pv.next) { - pv.assign(anySetterParameterObject); + pv.setValue(anySetterParameterObject); } _creatorParameters[_anySetter.getParameterIndex()] = anySetterParameterObject; - // mark as consumed, so current PropertyValueBuffer does not get reused. - _consumed = true; } return _creatorParameters; } @@ -299,12 +289,7 @@ public Object handleIdValue(final DeserializationContext ctxt, Object bean) thro return bean; } - protected PropertyValue buffered() { - if (_consumed) { - return null; - } - return _buffered; - } + protected PropertyValue buffered() { return _buffered; } public boolean isComplete() { return _paramsNeeded <= 0; } @@ -352,4 +337,8 @@ public void bufferAnyProperty(SettableAnyProperty prop, String propName, Object public void bufferMapProperty(Object key, Object value) { _buffered = new PropertyValue.Map(_buffered, value, key); } + + public void bufferAnyParameterProperty(SettableAnyProperty prop, String propName, Object value) { + _buffered = new PropertyValue.AnyParameter(_buffered, value, prop, propName); + } } From e9108756b4d9a3db5d83e98f3b38e74e0c897152 Mon Sep 17 00:00:00 2001 From: Tatu Saloranta Date: Sat, 8 Jun 2024 10:16:39 -0700 Subject: [PATCH 15/24] Minor comment addition --- .../jackson/databind/deser/BeanDeserializerFactory.java | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/src/main/java/com/fasterxml/jackson/databind/deser/BeanDeserializerFactory.java b/src/main/java/com/fasterxml/jackson/databind/deser/BeanDeserializerFactory.java index 0b26f14271..29bd13406d 100644 --- a/src/main/java/com/fasterxml/jackson/databind/deser/BeanDeserializerFactory.java +++ b/src/main/java/com/fasterxml/jackson/databind/deser/BeanDeserializerFactory.java @@ -818,15 +818,16 @@ protected void addInjectables(DeserializationContext ctxt, * for handling unknown bean properties, given a method that * has been designated as such setter. * - * @param mutator Either 2-argument method (setter, with key and value), or Field - * that contains Map; either way accessor used for passing "any values" + * @param mutator Either a 2-argument method (setter, with key and value), + * or a Field or (as of 2.18) Constructor Parameter of type Map or JsonNode/Object; + * either way accessor used for passing "any values" */ @SuppressWarnings("unchecked") protected SettableAnyProperty constructAnySetter(DeserializationContext ctxt, BeanDescription beanDesc, AnnotatedMember mutator) throws JsonMappingException { - //find the java type based on the annotated setter method or setter field + // find the java type based on the annotated setter method or setter field BeanProperty prop; JavaType keyType; JavaType valueType; @@ -840,6 +841,7 @@ protected SettableAnyProperty constructAnySetter(DeserializationContext ctxt, AnnotatedMethod am = (AnnotatedMethod) mutator; keyType = am.getParameterType(0); valueType = am.getParameterType(1); + // Need to resolve for possible generic types (like Maps, Collections) valueType = resolveMemberAndTypeAnnotations(ctxt, mutator, valueType); prop = new BeanProperty.Std(PropertyName.construct(mutator.getName()), valueType, null, mutator, From 93ad8708a70cbd71f1632cf46d43845e0c161bc0 Mon Sep 17 00:00:00 2001 From: Tatu Saloranta Date: Sat, 8 Jun 2024 10:21:51 -0700 Subject: [PATCH 16/24] Minor renaming, reordering, to unify handling (same order of type checks for Field vs Ctor Parameter) --- .../deser/BeanDeserializerFactory.java | 22 +++++++++---------- 1 file changed, 11 insertions(+), 11 deletions(-) diff --git a/src/main/java/com/fasterxml/jackson/databind/deser/BeanDeserializerFactory.java b/src/main/java/com/fasterxml/jackson/databind/deser/BeanDeserializerFactory.java index 29bd13406d..5c3c433db1 100644 --- a/src/main/java/com/fasterxml/jackson/databind/deser/BeanDeserializerFactory.java +++ b/src/main/java/com/fasterxml/jackson/databind/deser/BeanDeserializerFactory.java @@ -878,30 +878,30 @@ protected SettableAnyProperty constructAnySetter(DeserializationContext ctxt, } } else if (isParameter) { AnnotatedParameter af = (AnnotatedParameter) mutator; - JavaType fieldType = af.getType(); + JavaType paramType = af.getType(); parameterIndex = af.getIndex(); - if (fieldType.hasRawClass(JsonNode.class) || fieldType.hasRawClass(ObjectNode.class)) { - fieldType = resolveMemberAndTypeAnnotations(ctxt, mutator, fieldType); + if (paramType.isMapLikeType()) { + paramType = resolveMemberAndTypeAnnotations(ctxt, mutator, paramType); + keyType = paramType.getKeyType(); + valueType = paramType.getContentType(); + prop = new BeanProperty.Std(PropertyName.construct(mutator.getName()), + paramType, null, mutator, PropertyMetadata.STD_OPTIONAL); + } else if (paramType.hasRawClass(JsonNode.class) || paramType.hasRawClass(ObjectNode.class)) { + paramType = resolveMemberAndTypeAnnotations(ctxt, mutator, paramType); // Deserialize is individual values of ObjectNode, not full ObjectNode, so: valueType = ctxt.constructType(JsonNode.class); prop = new BeanProperty.Std(PropertyName.construct(mutator.getName()), - fieldType, null, mutator, PropertyMetadata.STD_OPTIONAL); + paramType, null, mutator, PropertyMetadata.STD_OPTIONAL); // Unlike with more complicated types, here we do not allow any annotation // overrides etc but instead short-cut handling: return SettableAnyProperty.constructForJsonNodeParameter(ctxt, prop, mutator, valueType, ctxt.findRootValueDeserializer(valueType), parameterIndex); - } else if (fieldType.isMapLikeType()) { - fieldType = resolveMemberAndTypeAnnotations(ctxt, mutator, fieldType); - keyType = fieldType.getKeyType(); - valueType = fieldType.getContentType(); - prop = new BeanProperty.Std(PropertyName.construct(mutator.getName()), - fieldType, null, mutator, PropertyMetadata.STD_OPTIONAL); } else { return ctxt.reportBadDefinition(beanDesc.getType(), String.format( "Unsupported type for any-setter: %s -- only support `Map`s, `JsonNode` and `ObjectNode` ", - ClassUtil.getTypeDescription(fieldType))); + ClassUtil.getTypeDescription(paramType))); } } else { return ctxt.reportBadDefinition(beanDesc.getType(), String.format( From b3a37ef59507a4d551c34834a264f98b9f371399 Mon Sep 17 00:00:00 2001 From: Tatu Saloranta Date: Sat, 8 Jun 2024 10:25:17 -0700 Subject: [PATCH 17/24] One more comment add --- .../jackson/databind/deser/BeanDeserializerFactory.java | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/src/main/java/com/fasterxml/jackson/databind/deser/BeanDeserializerFactory.java b/src/main/java/com/fasterxml/jackson/databind/deser/BeanDeserializerFactory.java index 5c3c433db1..80d9d492c2 100644 --- a/src/main/java/com/fasterxml/jackson/databind/deser/BeanDeserializerFactory.java +++ b/src/main/java/com/fasterxml/jackson/databind/deser/BeanDeserializerFactory.java @@ -909,6 +909,10 @@ protected SettableAnyProperty constructAnySetter(DeserializationContext ctxt, ClassUtil.nameOf(mutator.getClass()))); } + // NOTE: code from now on is for `Map` valued Any properties (JsonNode/ObjectNode + // already returned; unsupported types threw Exception), if we have Field/Ctor-Parameter + // any-setter -- or, basically Any supported type (if Method) + // First: see if there are explicitly specified // and then possible direct deserializer override on accessor KeyDeserializer keyDeser = findKeyDeserializerFromAnnotation(ctxt, mutator); From b5ef0d3b1afefddc634a1655a9275ddc6c737f22 Mon Sep 17 00:00:00 2001 From: Tatu Saloranta Date: Sat, 8 Jun 2024 14:31:13 -0700 Subject: [PATCH 18/24] minor comment improvement --- .../fasterxml/jackson/databind/deser/BeanDeserializer.java | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/main/java/com/fasterxml/jackson/databind/deser/BeanDeserializer.java b/src/main/java/com/fasterxml/jackson/databind/deser/BeanDeserializer.java index e83b11a26f..c36823415c 100644 --- a/src/main/java/com/fasterxml/jackson/databind/deser/BeanDeserializer.java +++ b/src/main/java/com/fasterxml/jackson/databind/deser/BeanDeserializer.java @@ -431,15 +431,15 @@ protected Object _deserializeUsingPropertyBased(final JsonParser p, final Deseri if (buffer.readIdProperty(propName) && creatorProp == null) { continue; } - // creator property? + // Creator property? if (creatorProp != null) { - // Last creator property to set? Object value; if ((activeView != null) && !creatorProp.visibleInView(activeView)) { p.skipChildren(); continue; } value = _deserializeWithErrorWrapping(p, ctxt, creatorProp); + // Last creator property to set? if (buffer.assignParameter(creatorProp, value)) { p.nextToken(); // to move to following FIELD_NAME/END_OBJECT Object bean; From 6cefc8eba9f7723b67a267ac1f27e5cf128e21bb Mon Sep 17 00:00:00 2001 From: Tatu Saloranta Date: Sat, 8 Jun 2024 14:47:27 -0700 Subject: [PATCH 19/24] Update tests slightly: no 1 failure as behavior not quite correct (wrt null/missing checks) --- .../deser/impl/PropertyValueBuffer.java | 21 +++++------ .../creators/AnySetterForCreator562Test.java | 37 ++++++++++++++----- 2 files changed, 36 insertions(+), 22 deletions(-) diff --git a/src/main/java/com/fasterxml/jackson/databind/deser/impl/PropertyValueBuffer.java b/src/main/java/com/fasterxml/jackson/databind/deser/impl/PropertyValueBuffer.java index 749b752e4f..2fe1f1641d 100644 --- a/src/main/java/com/fasterxml/jackson/databind/deser/impl/PropertyValueBuffer.java +++ b/src/main/java/com/fasterxml/jackson/databind/deser/impl/PropertyValueBuffer.java @@ -76,7 +76,7 @@ public class PropertyValueBuffer protected Object _idValue; /** - * [databind#562]: Allow use of `@JsonAnySetter` in `@JsonCreator` + * "Any setter" property; a Creator parameter (via {@code @JsonAnySetter}) * * @since 2.18 */ @@ -114,7 +114,6 @@ public PropertyValueBuffer(JsonParser p, DeserializationContext ctxt, int paramC this(p, ctxt, paramCount, oir, null); } - /** * Returns {@code true} if the given property was seen in the JSON source by * this buffer. @@ -184,7 +183,14 @@ public Object[] getParameters(SettableBeanProperty[] props) } } } - + // [databind#562] since 2.18 : Respect @JsonAnySetter in @JsonCreator + if ((_anySetter != null) && (_anySetter.getParameterIndex() >= 0)) { + Object anySetterParameterObject = _anySetter.createParameterObject(); + for (PropertyValue pv = this.buffered(); pv != null; pv = pv.next) { + pv.setValue(anySetterParameterObject); + } + _creatorParameters[_anySetter.getParameterIndex()] = anySetterParameterObject; + } if (_context.isEnabled(DeserializationFeature.FAIL_ON_NULL_CREATOR_PROPERTIES)) { for (int ix = 0; ix < props.length; ++ix) { if (_creatorParameters[ix] == null) { @@ -195,15 +201,6 @@ public Object[] getParameters(SettableBeanProperty[] props) } } } - - // [databind#562] since 2.18 : Respect @JsonAnySetter in @JsonCreator - if ((_anySetter != null) && (_anySetter.getParameterIndex() >= 0)) { - Object anySetterParameterObject = _anySetter.createParameterObject(); - for (PropertyValue pv = this.buffered(); pv != null; pv = pv.next) { - pv.setValue(anySetterParameterObject); - } - _creatorParameters[_anySetter.getParameterIndex()] = anySetterParameterObject; - } return _creatorParameters; } diff --git a/src/test/java/com/fasterxml/jackson/databind/deser/creators/AnySetterForCreator562Test.java b/src/test/java/com/fasterxml/jackson/databind/deser/creators/AnySetterForCreator562Test.java index 6eb6089827..4beec0d9ed 100644 --- a/src/test/java/com/fasterxml/jackson/databind/deser/creators/AnySetterForCreator562Test.java +++ b/src/test/java/com/fasterxml/jackson/databind/deser/creators/AnySetterForCreator562Test.java @@ -1,5 +1,6 @@ package com.fasterxml.jackson.databind.deser.creators; +import java.util.Collections; import java.util.HashMap; import java.util.Map; @@ -76,7 +77,7 @@ public PojoWithDisabled(@JsonProperty("a") String a, private final ObjectMapper MAPPER = newJsonMapper(); @Test - public void anySetterViaCreator562() throws Exception + public void mapAnySetterViaCreator562() throws Exception { Map expected = new HashMap<>(); expected.put("b", Integer.valueOf(42)); @@ -89,6 +90,11 @@ public void anySetterViaCreator562() throws Exception assertEquals("value", pojo.a); assertEquals(expected, pojo.stuff); + + // Should also initialize any-setter-Map even if no contents + pojo = MAPPER.readValue(a2q("{'a':'value2'}"), POJO562.class); + assertEquals("value2", pojo.a); + assertEquals(new HashMap<>(), pojo.stuff); } @Test @@ -103,20 +109,23 @@ public void testNodeAnySetterViaCreator562() throws Exception } @Test - public void testWithFailureConfigs562() throws Exception + public void testAnyMapWithNullCreatorProp() throws Exception { ObjectMapper failOnNullMapper = jsonMapperBuilder() .enable(DeserializationFeature.FAIL_ON_NULL_CREATOR_PROPERTIES).build(); - try { - failOnNullMapper.readValue(a2q("{'a':'value'}"), POJO562.class); - fail("Should not pass"); - } catch (MismatchedInputException e) { - verifyException(e, "Null value for creator property ''"); - } + // 08-Jun-2024, tatu: Should be fine as we get empty Map for "no any setters" + POJO562 value = failOnNullMapper.readValue(a2q("{'a':'value'}"), POJO562.class); + assertEquals(Collections.emptyMap(), value.stuff); + } + @Test + public void testAnyMapWithMissingCreatorProp() throws Exception + { ObjectMapper failOnMissingMapper = jsonMapperBuilder() .enable(DeserializationFeature.FAIL_ON_MISSING_CREATOR_PROPERTIES).build(); + + // Actually missing (no any props encountered) try { failOnMissingMapper.readValue(a2q("{'a':'value'}"), POJO562.class); fail("Should not pass"); @@ -124,6 +133,14 @@ public void testWithFailureConfigs562() throws Exception verifyException(e, "Missing creator property ''"); } + // But should NOT fail if we did find at least one... + POJO562 value = failOnMissingMapper.readValue(a2q("{'a':'value','b':'x'}"), POJO562.class); + assertEquals(Collections.singletonMap("b", "x"), value.stuff); + } + + @Test + public void testAnyMapWithNullOrMissingCreatorProp() throws Exception + { ObjectMapper failOnBothMapper = jsonMapperBuilder() .enable(DeserializationFeature.FAIL_ON_NULL_CREATOR_PROPERTIES) .enable(DeserializationFeature.FAIL_ON_MISSING_CREATOR_PROPERTIES) @@ -154,10 +171,10 @@ public void testAnySetterViaCreator562Disabled() throws Exception try { MAPPER.readValue(a2q("{'a':'value', 'b':42, 'c': 111}"), PojoWithDisabled.class); - fail(); + fail("Should not pass"); } catch (InvalidDefinitionException e) { verifyException(e, "Invalid type definition for type"); verifyException(e, "has no property name (and is not Injectable): can not use as property-based Creator"); } } -} \ No newline at end of file +} From e05336d4bad3414ce8b23c5ccb1eca77a6b8f324 Mon Sep 17 00:00:00 2001 From: Tatu Saloranta Date: Sat, 8 Jun 2024 17:05:03 -0700 Subject: [PATCH 20/24] Extend tests, fix an issue with combined buffering of non-creator vs any-creator params --- .../deser/impl/PropertyValueBuffer.java | 26 ++++++++----- .../creators/AnySetterForCreator562Test.java | 39 +++++++++++++++++++ 2 files changed, 56 insertions(+), 9 deletions(-) diff --git a/src/main/java/com/fasterxml/jackson/databind/deser/impl/PropertyValueBuffer.java b/src/main/java/com/fasterxml/jackson/databind/deser/impl/PropertyValueBuffer.java index 2fe1f1641d..fc5a54cec8 100644 --- a/src/main/java/com/fasterxml/jackson/databind/deser/impl/PropertyValueBuffer.java +++ b/src/main/java/com/fasterxml/jackson/databind/deser/impl/PropertyValueBuffer.java @@ -76,11 +76,18 @@ public class PropertyValueBuffer protected Object _idValue; /** - * "Any setter" property; a Creator parameter (via {@code @JsonAnySetter}) + * "Any setter" property bound to a Creator parameter (via {@code @JsonAnySetter}) * * @since 2.18 */ - protected final SettableAnyProperty _anySetter; + protected final SettableAnyProperty _anyParamSetter; + + /** + * If "Any-setter-via-Creator" exists, we will need to buffer values to feed it + * + * @since 2.18 + */ + protected PropertyValue _anyParamBuffered; /* /********************************************************** @@ -92,7 +99,7 @@ public class PropertyValueBuffer * @since 2.18 */ public PropertyValueBuffer(JsonParser p, DeserializationContext ctxt, int paramCount, - ObjectIdReader oir, SettableAnyProperty anySetter) + ObjectIdReader oir, SettableAnyProperty anyParamSetter) { _parser = p; _context = ctxt; @@ -104,7 +111,7 @@ public PropertyValueBuffer(JsonParser p, DeserializationContext ctxt, int paramC } else { _paramsSeenBig = new BitSet(); } - _anySetter = anySetter; + _anyParamSetter = anyParamSetter; } @Deprecated // since 2.18 @@ -184,12 +191,12 @@ public Object[] getParameters(SettableBeanProperty[] props) } } // [databind#562] since 2.18 : Respect @JsonAnySetter in @JsonCreator - if ((_anySetter != null) && (_anySetter.getParameterIndex() >= 0)) { - Object anySetterParameterObject = _anySetter.createParameterObject(); - for (PropertyValue pv = this.buffered(); pv != null; pv = pv.next) { + if ((_anyParamSetter != null) && (_anyParamSetter.getParameterIndex() >= 0)) { + Object anySetterParameterObject = _anyParamSetter.createParameterObject(); + for (PropertyValue pv = _anyParamBuffered; pv != null; pv = pv.next) { pv.setValue(anySetterParameterObject); } - _creatorParameters[_anySetter.getParameterIndex()] = anySetterParameterObject; + _creatorParameters[_anyParamSetter.getParameterIndex()] = anySetterParameterObject; } if (_context.isEnabled(DeserializationFeature.FAIL_ON_NULL_CREATOR_PROPERTIES)) { for (int ix = 0; ix < props.length; ++ix) { @@ -335,7 +342,8 @@ public void bufferMapProperty(Object key, Object value) { _buffered = new PropertyValue.Map(_buffered, value, key); } + // @since 2.18 public void bufferAnyParameterProperty(SettableAnyProperty prop, String propName, Object value) { - _buffered = new PropertyValue.AnyParameter(_buffered, value, prop, propName); + _anyParamBuffered = new PropertyValue.AnyParameter(_anyParamBuffered, value, prop, propName); } } diff --git a/src/test/java/com/fasterxml/jackson/databind/deser/creators/AnySetterForCreator562Test.java b/src/test/java/com/fasterxml/jackson/databind/deser/creators/AnySetterForCreator562Test.java index 4beec0d9ed..e31d5ee5e1 100644 --- a/src/test/java/com/fasterxml/jackson/databind/deser/creators/AnySetterForCreator562Test.java +++ b/src/test/java/com/fasterxml/jackson/databind/deser/creators/AnySetterForCreator562Test.java @@ -36,6 +36,22 @@ public POJO562(@JsonProperty("a") String a, } } + static class POJO562WithField + { + String a; + Map stuff; + + public String b; + + @JsonCreator + public POJO562WithField(@JsonProperty("a") String a, + @JsonAnySetter Map leftovers + ) { + this.a = a; + stuff = leftovers; + } + } + static class PojoWithNodeAnySetter { String a; @@ -91,10 +107,33 @@ public void mapAnySetterViaCreator562() throws Exception assertEquals("value", pojo.a); assertEquals(expected, pojo.stuff); + // Should be fine to vary ordering too + pojo = MAPPER.readValue(a2q( + "{'b':42, 'a':'value', 'c': 111}" + ), + POJO562.class); + + assertEquals("value", pojo.a); + assertEquals(expected, pojo.stuff); + // Should also initialize any-setter-Map even if no contents pojo = MAPPER.readValue(a2q("{'a':'value2'}"), POJO562.class); assertEquals("value2", pojo.a); assertEquals(new HashMap<>(), pojo.stuff); + + } + + // Creator and non-Creator props AND any-setter ought to be fine too + @Test + public void mapAnySetterViaCreatorAndField() throws Exception + { + POJO562WithField pojo = MAPPER.readValue( + a2q("{'a':'value', 'b':'xyz', 'c': 'abc'}"), + POJO562WithField.class); + + assertEquals("value", pojo.a); + assertEquals("xyz", pojo.b); + assertEquals(Collections.singletonMap("c", "abc"), pojo.stuff); } @Test From b482bd8fb5f92b4efc70704723b498d765d347fe Mon Sep 17 00:00:00 2001 From: Tatu Saloranta Date: Sat, 8 Jun 2024 17:06:44 -0700 Subject: [PATCH 21/24] Update release notes --- release-notes/CREDITS-2.x | 2 ++ release-notes/VERSION-2.x | 3 +++ 2 files changed, 5 insertions(+) diff --git a/release-notes/CREDITS-2.x b/release-notes/CREDITS-2.x index afa45fd5fb..b240f8e114 100644 --- a/release-notes/CREDITS-2.x +++ b/release-notes/CREDITS-2.x @@ -150,6 +150,8 @@ Chris Cleveland: Benson Margulies: * Reported #467: Unwanted POJO's embedded in tree via serialization to tree (2.4.0) + * Reported #562: Allow `@JsonAnySetter` to flow through Creators + (2.18.0) * Reported #601: ClassCastException for a custom serializer for enum key in `EnumMap` (2.4.4) * Contributed 944: Failure to use custom deserializer for key deserializer diff --git a/release-notes/VERSION-2.x b/release-notes/VERSION-2.x index 883e084301..c761d4760e 100644 --- a/release-notes/VERSION-2.x +++ b/release-notes/VERSION-2.x @@ -6,6 +6,9 @@ Project: jackson-databind 2.18.0 (not yet released) +#562: Allow `@JsonAnySetter` to flow through Creators + (reported by Benson M) + (fix by Joo-Hyuk K) #806: Problem with `NamingStrategy`, creator methods with implicit names #2977: Incompatible `FAIL_ON_MISSING_PRIMITIVE_PROPERTIES` and field level `@JsonProperty` From 22952bb752f89cbc91896d9387eec4769d3605ca Mon Sep 17 00:00:00 2001 From: Tatu Saloranta Date: Sat, 8 Jun 2024 17:23:38 -0700 Subject: [PATCH 22/24] Fix the failing test --- .../deser/impl/PropertyValueBuffer.java | 23 ++++++++++++++++--- 1 file changed, 20 insertions(+), 3 deletions(-) diff --git a/src/main/java/com/fasterxml/jackson/databind/deser/impl/PropertyValueBuffer.java b/src/main/java/com/fasterxml/jackson/databind/deser/impl/PropertyValueBuffer.java index fc5a54cec8..531c6ab24e 100644 --- a/src/main/java/com/fasterxml/jackson/databind/deser/impl/PropertyValueBuffer.java +++ b/src/main/java/com/fasterxml/jackson/databind/deser/impl/PropertyValueBuffer.java @@ -83,7 +83,8 @@ public class PropertyValueBuffer protected final SettableAnyProperty _anyParamSetter; /** - * If "Any-setter-via-Creator" exists, we will need to buffer values to feed it + * If "Any-setter-via-Creator" exists, we will need to buffer values to feed it, + * separate from regular, non-creator properties (see {@code _buffered}). * * @since 2.18 */ @@ -111,7 +112,12 @@ public PropertyValueBuffer(JsonParser p, DeserializationContext ctxt, int paramC } else { _paramsSeenBig = new BitSet(); } - _anyParamSetter = anyParamSetter; + // Only care about Creator-bound Any setters: + if ((anyParamSetter == null) || (anyParamSetter.getParameterIndex() < 0)) { + _anyParamSetter = null; + } else { + _anyParamSetter = anyParamSetter; + } } @Deprecated // since 2.18 @@ -191,7 +197,7 @@ public Object[] getParameters(SettableBeanProperty[] props) } } // [databind#562] since 2.18 : Respect @JsonAnySetter in @JsonCreator - if ((_anyParamSetter != null) && (_anyParamSetter.getParameterIndex() >= 0)) { + if (_anyParamSetter != null) { Object anySetterParameterObject = _anyParamSetter.createParameterObject(); for (PropertyValue pv = _anyParamBuffered; pv != null; pv = pv.next) { pv.setValue(anySetterParameterObject); @@ -213,6 +219,17 @@ public Object[] getParameters(SettableBeanProperty[] props) protected Object _findMissing(SettableBeanProperty prop) throws JsonMappingException { + // 08-Jun-2024: [databind#562] AnySetters are bit special + if (_anyParamSetter != null) { + if (prop.getCreatorIndex() == _anyParamSetter.getParameterIndex()) { + // Ok if anything buffered + if (_anyParamBuffered != null) { + // ... will be assigned by caller later on, for now return null + return null; + } + } + } + // First: do we have injectable value? Object injectableValueId = prop.getInjectableValueId(); if (injectableValueId != null) { From 01c0cc536df80699f27cbd4f11fe124101a2060b Mon Sep 17 00:00:00 2001 From: Tatu Saloranta Date: Sat, 8 Jun 2024 18:01:05 -0700 Subject: [PATCH 23/24] Minor error reporting improvement --- .../databind/deser/BasicDeserializerFactory.java | 10 ++++++---- .../jackson/databind/deser/SettableAnyProperty.java | 1 + .../jackson/databind/deser/impl/CreatorCollector.java | 1 + 3 files changed, 8 insertions(+), 4 deletions(-) diff --git a/src/main/java/com/fasterxml/jackson/databind/deser/BasicDeserializerFactory.java b/src/main/java/com/fasterxml/jackson/databind/deser/BasicDeserializerFactory.java index 69f51fc2ef..ec99b8f7f7 100644 --- a/src/main/java/com/fasterxml/jackson/databind/deser/BasicDeserializerFactory.java +++ b/src/main/java/com/fasterxml/jackson/databind/deser/BasicDeserializerFactory.java @@ -508,7 +508,7 @@ private void _addSelectedPropertiesBasedCreator(DeserializationContext ctxt, { final int paramCount = candidate.paramCount(); SettableBeanProperty[] properties = new SettableBeanProperty[paramCount]; - boolean anySetterFound = false; + int anySetterIx = -1; for (int i = 0; i < paramCount; ++i) { JacksonInject.Value injectId = candidate.injection(i); @@ -516,10 +516,12 @@ private void _addSelectedPropertiesBasedCreator(DeserializationContext ctxt, PropertyName name = candidate.paramName(i); boolean isAnySetter = Boolean.TRUE.equals(ctxt.getAnnotationIntrospector().hasAnySetter(param)); if (isAnySetter) { - if (anySetterFound) { - ctxt.reportBadTypeDefinition(beanDesc, "More than one 'any-setter' specified"); + if (anySetterIx >= 0) { + ctxt.reportBadTypeDefinition(beanDesc, + "More than one 'any-setter' specified (parameter #%d vs #%d)", + anySetterIx, i); } else { - anySetterFound = true; + anySetterIx = i; } } else if (name == null) { // 21-Sep-2017, tatu: Looks like we want to block accidental use of Unwrapped, diff --git a/src/main/java/com/fasterxml/jackson/databind/deser/SettableAnyProperty.java b/src/main/java/com/fasterxml/jackson/databind/deser/SettableAnyProperty.java index f165b1ebe3..0cb738ada7 100644 --- a/src/main/java/com/fasterxml/jackson/databind/deser/SettableAnyProperty.java +++ b/src/main/java/com/fasterxml/jackson/databind/deser/SettableAnyProperty.java @@ -513,6 +513,7 @@ public SettableAnyProperty withValueDeserializer(JsonDeserializer deser) _valueTypeDeserializer, _valueInstantiator, _parameterIndex); } + @SuppressWarnings("unchecked") @Override protected void _set(Object instance, Object propName, Object value) { diff --git a/src/main/java/com/fasterxml/jackson/databind/deser/impl/CreatorCollector.java b/src/main/java/com/fasterxml/jackson/databind/deser/impl/CreatorCollector.java index 963300a84b..2a0948ded9 100644 --- a/src/main/java/com/fasterxml/jackson/databind/deser/impl/CreatorCollector.java +++ b/src/main/java/com/fasterxml/jackson/databind/deser/impl/CreatorCollector.java @@ -184,6 +184,7 @@ public void addPropertyCreator(AnnotatedWithParams creator, String name = properties[i].getName(); // Need to consider Injectables, which may not have // a name at all, and need to be skipped + // (same for possible AnySetter) if (name.isEmpty() && (properties[i].getInjectableValueId() != null)) { continue; } From 34c04116d89f70bf0e6776ec207bc4b94ce6573e Mon Sep 17 00:00:00 2001 From: Tatu Saloranta Date: Sat, 8 Jun 2024 18:06:37 -0700 Subject: [PATCH 24/24] Test improvement --- .../databind/deser/creators/AnySetterForCreator562Test.java | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/src/test/java/com/fasterxml/jackson/databind/deser/creators/AnySetterForCreator562Test.java b/src/test/java/com/fasterxml/jackson/databind/deser/creators/AnySetterForCreator562Test.java index e31d5ee5e1..e0ec28ac24 100644 --- a/src/test/java/com/fasterxml/jackson/databind/deser/creators/AnySetterForCreator562Test.java +++ b/src/test/java/com/fasterxml/jackson/databind/deser/creators/AnySetterForCreator562Test.java @@ -145,6 +145,12 @@ public void testNodeAnySetterViaCreator562() throws Exception assertEquals("value", pojo.a); assertEquals(a2q("{'c':111,'b':42}"), pojo.anySetterNode + ""); + + // Also ok to get nothing, resulting in empty ObjectNode + pojo = MAPPER.readValue(a2q("{'a':'ok'}"), PojoWithNodeAnySetter.class); + + assertEquals("ok", pojo.a); + assertEquals(MAPPER.createObjectNode(), pojo.anySetterNode); } @Test