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..bac17ff3d3 100644 --- a/src/main/java/com/fasterxml/jackson/databind/deser/BasicDeserializerFactory.java +++ b/src/main/java/com/fasterxml/jackson/databind/deser/BasicDeserializerFactory.java @@ -401,7 +401,7 @@ private void _addImplicitDelegatingConstructors(DeserializationContext ctxt, if (injectable != null) { ++injectCount; - properties[i] = constructCreatorProperty(ctxt, beanDesc, null, i, param, injectable); + properties[i] = constructCreatorProperty(ctxt, beanDesc, null, i, param, injectable, false); continue; } NameTransformer unwrapper = intr.findUnwrappingNameTransformer(param); @@ -472,7 +472,7 @@ private boolean _addExplicitDelegatingCreator(DeserializationContext ctxt, AnnotatedParameter param = candidate.parameter(i); JacksonInject.Value injectId = candidate.injection(i); if (injectId != null) { - properties[i] = constructCreatorProperty(ctxt, beanDesc, null, i, param, injectId); + properties[i] = constructCreatorProperty(ctxt, beanDesc, null, i, param, injectId, false); continue; } if (ix < 0) { @@ -508,6 +508,7 @@ private void _addSelectedPropertiesBasedCreator(DeserializationContext ctxt, { final int paramCount = candidate.paramCount(); SettableBeanProperty[] properties = new SettableBeanProperty[paramCount]; + int anySetterIndex = -1; for (int i = 0; i < paramCount; ++i) { JacksonInject.Value injectId = candidate.injection(i); @@ -520,14 +521,25 @@ private void _addSelectedPropertiesBasedCreator(DeserializationContext ctxt, if (unwrapper != null) { _reportUnwrappedCreatorProperty(ctxt, beanDesc, param); } - // Must be injectable or have name; without either won't work - if ((name == null) && (injectId == null)) { - ctxt.reportBadTypeDefinition(beanDesc, + // [databind#562] Allow @JsonAnySetter in creators + if (Boolean.TRUE.equals(ctxt.getAnnotationIntrospector().hasAnySetter(param))) { + if (anySetterIndex >= 0) { + ctxt.reportBadTypeDefinition(beanDesc, + "More than one 'any-setter' specified (parameters #%d vs #%d)", + anySetterIndex, i); + } + anySetterIndex = i; + } else { + // 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); + } } } - properties[i] = constructCreatorProperty(ctxt, beanDesc, name, i, param, injectId); + properties[i] = constructCreatorProperty(ctxt, beanDesc, name, i, param, injectId, + anySetterIndex == i); } creators.addPropertyCreator(candidate.creator(), true, properties); } @@ -600,11 +612,14 @@ private void _reportUnwrappedCreatorProperty(DeserializationContext ctxt, * Method that will construct a property object that represents * a logical property passed via Creator (constructor or static * factory method) + * + * @since 2.18 */ protected SettableBeanProperty constructCreatorProperty(DeserializationContext ctxt, BeanDescription beanDesc, PropertyName name, int index, AnnotatedParameter param, - JacksonInject.Value injectable) + JacksonInject.Value injectable, + boolean hasAnySetter) throws JsonMappingException { final DeserializationConfig config = ctxt.getConfig(); @@ -642,7 +657,7 @@ protected SettableBeanProperty constructCreatorProperty(DeserializationContext c // so it is not called directly here SettableBeanProperty prop = CreatorProperty.construct(name, type, property.getWrapperName(), typeDeser, beanDesc.getClassAnnotations(), param, index, injectable, - metadata); + metadata, hasAnySetter); JsonDeserializer deser = findDeserializerFromAnnotation(ctxt, param); if (deser == null) { deser = type.getValueHandler(); @@ -655,6 +670,20 @@ protected SettableBeanProperty constructCreatorProperty(DeserializationContext c return prop; } + /** + * @deprecated since 2.18, use {@link #constructCreatorProperty(DeserializationContext, BeanDescription, + * PropertyName, int, AnnotatedParameter, JacksonInject.Value, boolean)} instead. + */ + @Deprecated + protected SettableBeanProperty constructCreatorProperty(DeserializationContext ctxt, + BeanDescription beanDesc, PropertyName name, int index, + AnnotatedParameter param, + JacksonInject.Value injectable) + throws JsonMappingException + { + return constructCreatorProperty(ctxt, beanDesc, name, index, param, injectable, false); + } + /** * Helper method copied from {@code POJOPropertyBuilder} since that won't be * applied to creator parameters 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..0f87d767fa 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; @@ -497,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.bufferAnySetter(ctxt, p, creator, propName); } catch (Exception e) { wrapAndThrow(e, _beanType.getRawClass(), propName, ctxt); } 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..6c89fdaa47 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); 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... @@ -558,6 +561,7 @@ protected void addBeanProps(DeserializationContext ctxt, } } } + final boolean useGettersAsSetters = ctxt.isEnabled(MapperFeature.USE_GETTERS_AS_SETTERS) && ctxt.isEnabled(MapperFeature.AUTO_DETECT_GETTERS); @@ -661,6 +665,18 @@ protected void addBeanProps(DeserializationContext ctxt, } } + // since 2.18 + private AnnotatedMember _findCreatorPropWithAnySetter(SettableBeanProperty[] creatorProps) { + if (creatorProps != null) { + for (SettableBeanProperty prop : creatorProps) { + if (((CreatorProperty) prop).isAnySetter()) { + return prop.getMember(); + } + } + } + return null; + } + private boolean _isSetterlessType(Class rawType) { // May also need to consider getters // for Map/Collection properties; but with lowest precedence @@ -808,6 +824,7 @@ protected SettableAnyProperty constructAnySetter(DeserializationContext ctxt, JavaType keyType; JavaType valueType; final boolean isField = mutator instanceof AnnotatedField; + boolean isMapParam = false; if (mutator instanceof AnnotatedMethod) { // we know it's a 2-arg method, second arg is the value @@ -819,6 +836,22 @@ protected SettableAnyProperty constructAnySetter(DeserializationContext ctxt, 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.NO_NAME, + fieldType, null, mutator, PropertyMetadata.STD_OPTIONAL); + isMapParam = true; } else if (isField) { AnnotatedField af = (AnnotatedField) mutator; // get the type from the content type of the map object @@ -880,6 +913,11 @@ protected SettableAnyProperty constructAnySetter(DeserializationContext ctxt, return SettableAnyProperty.constructForMapField(ctxt, prop, mutator, valueType, keyDeser, deser, typeDeser); } + // [databind#562] Allow @JsonAnySetter in creators + if (isMapParam) { + return SettableAnyProperty.constructForMapParameter(ctxt, + prop, mutator, valueType, keyDeser, deser, typeDeser); + } return SettableAnyProperty.constructForMethod(ctxt, prop, mutator, valueType, keyDeser, deser, typeDeser); } diff --git a/src/main/java/com/fasterxml/jackson/databind/deser/CreatorProperty.java b/src/main/java/com/fasterxml/jackson/databind/deser/CreatorProperty.java index 5e7ce8c98d..ea79d4caa5 100644 --- a/src/main/java/com/fasterxml/jackson/databind/deser/CreatorProperty.java +++ b/src/main/java/com/fasterxml/jackson/databind/deser/CreatorProperty.java @@ -2,6 +2,7 @@ import java.io.IOException; import java.lang.annotation.Annotation; +import java.util.Map; import com.fasterxml.jackson.annotation.JacksonInject; import com.fasterxml.jackson.core.JsonParser; @@ -76,19 +77,42 @@ public class CreatorProperty protected boolean _ignorable; /** - * @since 2.11 + * Marker flag to indicate that current property is used to handle "any setter" via `@JsonAnySetter`. + * + * @since 2.18 + */ + protected final boolean _isAnySetter; + + /** + * @since 2.18 */ protected CreatorProperty(PropertyName name, JavaType type, PropertyName wrapperName, TypeDeserializer typeDeser, Annotations contextAnnotations, AnnotatedParameter param, int index, JacksonInject.Value injectable, - PropertyMetadata metadata) + PropertyMetadata metadata, boolean isAnySetter) { super(name, type, wrapperName, typeDeser, contextAnnotations, metadata); _annotated = param; _creatorIndex = index; _injectableValue = injectable; _fallbackSetter = null; + _isAnySetter = isAnySetter; // [databind#562] since 2.18 + } + + /** + * @since 2.11 + * @deprecated since 2.18. use later version instead. + */ + @Deprecated + protected CreatorProperty(PropertyName name, JavaType type, PropertyName wrapperName, + TypeDeserializer typeDeser, + Annotations contextAnnotations, AnnotatedParameter param, + int index, JacksonInject.Value injectable, + PropertyMetadata metadata) + { + this(name, type, wrapperName, typeDeser, contextAnnotations, param, index, injectable, + metadata, false); } /** @@ -122,9 +146,26 @@ public CreatorProperty(PropertyName name, JavaType type, PropertyName wrapperNam * method parameter; used for accessing annotations of the property * @param injectable Information about injectable value, if any * @param index Index of this property within creator invocation - * + * @param isAnySetter Marker flag to indicate that current property is used to handle "any setter" via `@JsonAnySetter`. + * + * @since 2.18 + */ + public static CreatorProperty construct(PropertyName name, JavaType type, PropertyName wrapperName, + TypeDeserializer typeDeser, + Annotations contextAnnotations, AnnotatedParameter param, + int index, JacksonInject.Value injectable, + PropertyMetadata metadata, + boolean isAnySetter) + { + return new CreatorProperty(name, type, wrapperName, typeDeser, contextAnnotations, + param, index, injectable, metadata, isAnySetter); + } + + /** * @since 2.11 + * @deprecated since 2.18. use later version instead. */ + @Deprecated public static CreatorProperty construct(PropertyName name, JavaType type, PropertyName wrapperName, TypeDeserializer typeDeser, Annotations contextAnnotations, AnnotatedParameter param, @@ -132,7 +173,7 @@ public static CreatorProperty construct(PropertyName name, JavaType type, Proper PropertyMetadata metadata) { return new CreatorProperty(name, type, wrapperName, typeDeser, contextAnnotations, - param, index, injectable, metadata); + param, index, injectable, metadata, false); } /** @@ -145,6 +186,7 @@ protected CreatorProperty(CreatorProperty src, PropertyName newName) { _fallbackSetter = src._fallbackSetter; _creatorIndex = src._creatorIndex; _ignorable = src._ignorable; + _isAnySetter = src._isAnySetter; // [databind#562] since 2.18 } protected CreatorProperty(CreatorProperty src, JsonDeserializer deser, @@ -155,6 +197,7 @@ protected CreatorProperty(CreatorProperty src, JsonDeserializer deser, _fallbackSetter = src._fallbackSetter; _creatorIndex = src._creatorIndex; _ignorable = src._ignorable; + _isAnySetter = src._isAnySetter; // [databind#562] since 2.18 } @Override @@ -320,6 +363,13 @@ public boolean isInjectionOnly() { // public boolean isInjectionOnly() { return false; } + /** + * @since 2.18 + */ + public boolean isAnySetter() { + return _isAnySetter; + } + /* /********************************************************** /* Overridden methods, other @@ -354,4 +404,13 @@ private void _reportMissingSetter(JsonParser p, DeserializationContext ctxt) thr throw InvalidDefinitionException.from(p, msg, getType()); } } + + /** + * @since 2.18 + */ + public Map createAnyPropertyMap(DeserializationContext context, SettableAnyProperty anySetter) + throws IOException + { + return ((SettableAnyProperty.MapParamAnyProperty) anySetter).createAnyPropertyMap(context); + } } 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..1c257e759f 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,28 @@ 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 +460,49 @@ 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) throws Exception + { + throw new UnsupportedOperationException("Cannot set any properties for constructor parameter of type `Map`"); + } + + @SuppressWarnings("unchecked") + protected Map createAnyPropertyMap(DeserializationContext ctxt) + throws IOException + { + return (Map) _valueInstantiator.createUsingDefault(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 5225baaab6..ffcef5553c 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,8 @@ import com.fasterxml.jackson.core.JsonParser; import com.fasterxml.jackson.databind.*; +import com.fasterxml.jackson.databind.deser.CreatorProperty; +import com.fasterxml.jackson.databind.deser.SettableAnyProperty; import com.fasterxml.jackson.databind.deser.SettableBeanProperty; import com.fasterxml.jackson.databind.deser.ValueInstantiator; @@ -187,6 +189,16 @@ public SettableBeanProperty findCreatorProperty(int propertyIndex) { /********************************************************** */ + /** + * 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); + } + /** * Method called when starting to build a bean instance. * @@ -214,6 +226,20 @@ public Object build(DeserializationContext ctxt, PropertyValueBuffer buffer) thr return bean; } + public CreatorProperty findAnySetterProperty() { + for (SettableBeanProperty prop : _allProperties) { + if (!(prop instanceof CreatorProperty)) { + continue; + } + CreatorProperty cp = (CreatorProperty) prop; + if (!cp.isAnySetter()) { + continue; + } + return cp; + } + return null; + } + /* /********************************************************** /* 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 5d323381a0..908eabcea9 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,10 +2,12 @@ import java.io.IOException; import java.util.BitSet; +import java.util.Map; import com.fasterxml.jackson.core.JsonParser; import com.fasterxml.jackson.databind.*; +import com.fasterxml.jackson.databind.deser.CreatorProperty; import com.fasterxml.jackson.databind.deser.SettableAnyProperty; import com.fasterxml.jackson.databind.deser.SettableBeanProperty; import com.fasterxml.jackson.databind.introspect.AnnotatedMember; @@ -75,6 +77,20 @@ public class PropertyValueBuffer */ protected Object _idValue; + /** + * [databind#562]: Allow use of `@JsonAnySetter` in `@JsonCreator` + * + * @since 2.18 + */ + protected final SettableAnyProperty _anySetter; + + /** + * [databind#562]: Allow use of `@JsonAnySetter` in `@JsonCreator` + * + * @since 2.18 + */ + private Map _anyPropertyMap; + /* /********************************************************** /* Life-cycle @@ -82,7 +98,7 @@ public class PropertyValueBuffer */ public PropertyValueBuffer(JsonParser p, DeserializationContext ctxt, int paramCount, - ObjectIdReader oir) + ObjectIdReader oir, SettableAnyProperty anySetter) { _parser = p; _context = ctxt; @@ -94,6 +110,17 @@ public PropertyValueBuffer(JsonParser p, DeserializationContext ctxt, int paramC } else { _paramsSeenBig = new BitSet(); } + _anySetter = anySetter; + } + + /** + * + * @deprecated since 2.18, use later version instead. + */ + public PropertyValueBuffer(JsonParser p, DeserializationContext ctxt, int paramCount, + ObjectIdReader oir) + { + this(p, ctxt, paramCount, oir, null); } /** @@ -147,28 +174,29 @@ public Object getParameter(SettableBeanProperty prop) public Object[] getParameters(SettableBeanProperty[] props) throws JsonMappingException { + final Object[] creatorParams = _creatorParameters; // quick check to see if anything else is needed if (_paramsNeeded > 0) { if (_paramsSeenBig == null) { int mask = _paramsSeen; // not optimal, could use `Integer.trailingZeroes()`, but for now should not // really matter for common cases - for (int ix = 0, len = _creatorParameters.length; ix < len; ++ix, mask >>= 1) { + for (int ix = 0, len = creatorParams.length; ix < len; ++ix, mask >>= 1) { if ((mask & 1) == 0) { - _creatorParameters[ix] = _findMissing(props[ix]); + creatorParams[ix] = _findMissing(props[ix]); } } } else { - final int len = _creatorParameters.length; + final int len = creatorParams.length; for (int ix = 0; (ix = _paramsSeenBig.nextClearBit(ix)) < len; ++ix) { - _creatorParameters[ix] = _findMissing(props[ix]); + creatorParams[ix] = _findMissing(props[ix]); } } } if (_context.isEnabled(DeserializationFeature.FAIL_ON_NULL_CREATOR_PROPERTIES)) { for (int ix = 0; ix < props.length; ++ix) { - if (_creatorParameters[ix] == null) { + if (creatorParams[ix] == null) { SettableBeanProperty prop = props[ix]; _context.reportInputMismatch(prop, "Null value for creator property '%s' (index %d); `DeserializationFeature.FAIL_ON_NULL_CREATOR_PROPERTIES` enabled", @@ -176,7 +204,16 @@ public Object[] getParameters(SettableBeanProperty[] props) } } } - return _creatorParameters; + // [databind#562] since 2.18 : Respect @JsonAnySetter in @JsonCreator + if (_anySetter != null) { + for (int i = 0; i < creatorParams.length; i++) { + if (props[i].getMember() == _anySetter.getProperty().getMember()) { + creatorParams[i] = _anyPropertyMap; + break; + } + } + } + return creatorParams; } protected Object _findMissing(SettableBeanProperty prop) throws JsonMappingException @@ -309,4 +346,25 @@ public void bufferAnyProperty(SettableAnyProperty prop, String propName, Object public void bufferMapProperty(Object key, Object value) { _buffered = new PropertyValue.Map(_buffered, value, key); } + + /** + * @since 2.18 + */ + public void bufferAnySetter(DeserializationContext ctxt, JsonParser p, PropertyBasedCreator creator, String propName) + throws IOException { + // Only called once, to initialize map + if (_anyPropertyMap == null) { + CreatorProperty cp = creator.findAnySetterProperty(); + if (cp == null) { + ctxt.reportBadDefinition(creator.getClass(), + "Invalid configuration: no creator property with 'any-setter' annotation found"); + } + try { + _anyPropertyMap = cp.createAnyPropertyMap(_context, _anySetter); + } catch (IOException e) { + _context.reportInputMismatch(cp, e.getMessage()); + } + } + _anyPropertyMap.put(propName, _anySetter.deserialize(p, ctxt)); + } } 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..e3a3540b15 --- /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"); + } + } +} 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); - } -}