Skip to content

Commit 65a00fd

Browse files
authored
Allow @JsonAnySetter on Creators (#4558)
1 parent daf85f2 commit 65a00fd

File tree

13 files changed

+635
-62
lines changed

13 files changed

+635
-62
lines changed

release-notes/CREDITS-2.x

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -150,6 +150,8 @@ Chris Cleveland:
150150
Benson Margulies:
151151
* Reported #467: Unwanted POJO's embedded in tree via serialization to tree
152152
(2.4.0)
153+
* Reported #562: Allow `@JsonAnySetter` to flow through Creators
154+
(2.18.0)
153155
* Reported #601: ClassCastException for a custom serializer for enum key in `EnumMap`
154156
(2.4.4)
155157
* Contributed 944: Failure to use custom deserializer for key deserializer

release-notes/VERSION-2.x

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,9 @@ Project: jackson-databind
66

77
2.18.0 (not yet released)
88

9+
#562: Allow `@JsonAnySetter` to flow through Creators
10+
(reported by Benson M)
11+
(fix by Joo-Hyuk K)
912
#806: Problem with `NamingStrategy`, creator methods with implicit names
1013
#2977: Incompatible `FAIL_ON_MISSING_PRIMITIVE_PROPERTIES` and
1114
field level `@JsonProperty`

src/main/java/com/fasterxml/jackson/databind/deser/BasicDeserializerFactory.java

Lines changed: 11 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -508,12 +508,22 @@ private void _addSelectedPropertiesBasedCreator(DeserializationContext ctxt,
508508
{
509509
final int paramCount = candidate.paramCount();
510510
SettableBeanProperty[] properties = new SettableBeanProperty[paramCount];
511+
int anySetterIx = -1;
511512

512513
for (int i = 0; i < paramCount; ++i) {
513514
JacksonInject.Value injectId = candidate.injection(i);
514515
AnnotatedParameter param = candidate.parameter(i);
515516
PropertyName name = candidate.paramName(i);
516-
if (name == null) {
517+
boolean isAnySetter = Boolean.TRUE.equals(ctxt.getAnnotationIntrospector().hasAnySetter(param));
518+
if (isAnySetter) {
519+
if (anySetterIx >= 0) {
520+
ctxt.reportBadTypeDefinition(beanDesc,
521+
"More than one 'any-setter' specified (parameter #%d vs #%d)",
522+
anySetterIx, i);
523+
} else {
524+
anySetterIx = i;
525+
}
526+
} else if (name == null) {
517527
// 21-Sep-2017, tatu: Looks like we want to block accidental use of Unwrapped,
518528
// as that will not work with Creators well at all
519529
NameTransformer unwrapper = ctxt.getAnnotationIntrospector().findUnwrappingNameTransformer(param);

src/main/java/com/fasterxml/jackson/databind/deser/BeanDeserializer.java

Lines changed: 6 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -415,7 +415,9 @@ protected Object _deserializeUsingPropertyBased(final JsonParser p, final Deseri
415415
throws IOException
416416
{
417417
final PropertyBasedCreator creator = _propertyBasedCreator;
418-
PropertyValueBuffer buffer = creator.startBuilding(p, ctxt, _objectIdReader);
418+
PropertyValueBuffer buffer = (_anySetter != null)
419+
? creator.startBuildingWithAnySetter(p, ctxt, _objectIdReader, _anySetter)
420+
: creator.startBuilding(p, ctxt, _objectIdReader);
419421
TokenBuffer unknown = null;
420422
final Class<?> activeView = _needViewProcesing ? ctxt.getActiveView() : null;
421423

@@ -429,15 +431,15 @@ protected Object _deserializeUsingPropertyBased(final JsonParser p, final Deseri
429431
if (buffer.readIdProperty(propName) && creatorProp == null) {
430432
continue;
431433
}
432-
// creator property?
434+
// Creator property?
433435
if (creatorProp != null) {
434-
// Last creator property to set?
435436
Object value;
436437
if ((activeView != null) && !creatorProp.visibleInView(activeView)) {
437438
p.skipChildren();
438439
continue;
439440
}
440441
value = _deserializeWithErrorWrapping(p, ctxt, creatorProp);
442+
// Last creator property to set?
441443
if (buffer.assignParameter(creatorProp, value)) {
442444
p.nextToken(); // to move to following FIELD_NAME/END_OBJECT
443445
Object bean;
@@ -497,7 +499,7 @@ protected Object _deserializeUsingPropertyBased(final JsonParser p, final Deseri
497499
// "any property"?
498500
if (_anySetter != null) {
499501
try {
500-
buffer.bufferAnyProperty(_anySetter, propName, _anySetter.deserialize(p, ctxt));
502+
buffer.bufferAnyParameterProperty(_anySetter, propName, _anySetter.deserialize(p, ctxt));
501503
} catch (Exception e) {
502504
wrapAndThrow(e, _beanType.getRawClass(), propName, ctxt);
503505
}

src/main/java/com/fasterxml/jackson/databind/deser/BeanDeserializerFactory.java

Lines changed: 69 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -543,9 +543,9 @@ protected void addBeanProps(DeserializationContext ctxt,
543543
}
544544

545545
// Also, do we have a fallback "any" setter?
546-
AnnotatedMember anySetter = beanDesc.findAnySetterAccessor();
546+
SettableAnyProperty anySetter = _resolveAnySetter(ctxt, beanDesc, creatorProps);
547547
if (anySetter != null) {
548-
builder.setAnySetter(constructAnySetter(ctxt, beanDesc, anySetter));
548+
builder.setAnySetter(anySetter);
549549
} else {
550550
// 23-Jan-2018, tatu: although [databind#1805] would suggest we should block
551551
// properties regardless, for now only consider unless there's any setter...
@@ -661,6 +661,29 @@ protected void addBeanProps(DeserializationContext ctxt,
661661
}
662662
}
663663

664+
// since 2.18
665+
private SettableAnyProperty _resolveAnySetter(DeserializationContext ctxt,
666+
BeanDescription beanDesc, SettableBeanProperty[] creatorProps)
667+
throws JsonMappingException
668+
{
669+
// Find the regular method/field level any-setter
670+
AnnotatedMember anySetter = beanDesc.findAnySetterAccessor();
671+
if (anySetter != null) {
672+
return constructAnySetter(ctxt, beanDesc, anySetter);
673+
}
674+
// else look for any-setter via @JsonCreator
675+
if (creatorProps != null) {
676+
for (SettableBeanProperty prop : creatorProps) {
677+
AnnotatedMember member = prop.getMember();
678+
if (member != null && Boolean.TRUE.equals(ctxt.getAnnotationIntrospector().hasAnySetter(member))) {
679+
return constructAnySetter(ctxt, beanDesc, member);
680+
}
681+
}
682+
}
683+
// not found, that's fine, too
684+
return null;
685+
}
686+
664687
private boolean _isSetterlessType(Class<?> rawType) {
665688
// May also need to consider getters
666689
// for Map/Collection properties; but with lowest precedence
@@ -795,25 +818,30 @@ protected void addInjectables(DeserializationContext ctxt,
795818
* for handling unknown bean properties, given a method that
796819
* has been designated as such setter.
797820
*
798-
* @param mutator Either 2-argument method (setter, with key and value), or Field
799-
* that contains Map; either way accessor used for passing "any values"
821+
* @param mutator Either a 2-argument method (setter, with key and value),
822+
* or a Field or (as of 2.18) Constructor Parameter of type Map or JsonNode/Object;
823+
* either way accessor used for passing "any values"
800824
*/
801825
@SuppressWarnings("unchecked")
802826
protected SettableAnyProperty constructAnySetter(DeserializationContext ctxt,
803827
BeanDescription beanDesc, AnnotatedMember mutator)
804828
throws JsonMappingException
805829
{
806-
//find the java type based on the annotated setter method or setter field
830+
// find the java type based on the annotated setter method or setter field
807831
BeanProperty prop;
808832
JavaType keyType;
809833
JavaType valueType;
810834
final boolean isField = mutator instanceof AnnotatedField;
835+
// [databind#562] Allow @JsonAnySetter on Creator constructor
836+
final boolean isParameter = mutator instanceof AnnotatedParameter;
837+
int parameterIndex = -1;
811838

812839
if (mutator instanceof AnnotatedMethod) {
813840
// we know it's a 2-arg method, second arg is the value
814841
AnnotatedMethod am = (AnnotatedMethod) mutator;
815842
keyType = am.getParameterType(0);
816843
valueType = am.getParameterType(1);
844+
// Need to resolve for possible generic types (like Maps, Collections)
817845
valueType = resolveMemberAndTypeAnnotations(ctxt, mutator, valueType);
818846
prop = new BeanProperty.Std(PropertyName.construct(mutator.getName()),
819847
valueType, null, mutator,
@@ -848,11 +876,43 @@ protected SettableAnyProperty constructAnySetter(DeserializationContext ctxt,
848876
"Unsupported type for any-setter: %s -- only support `Map`s, `JsonNode` and `ObjectNode` ",
849877
ClassUtil.getTypeDescription(fieldType)));
850878
}
879+
} else if (isParameter) {
880+
AnnotatedParameter af = (AnnotatedParameter) mutator;
881+
JavaType paramType = af.getType();
882+
parameterIndex = af.getIndex();
883+
884+
if (paramType.isMapLikeType()) {
885+
paramType = resolveMemberAndTypeAnnotations(ctxt, mutator, paramType);
886+
keyType = paramType.getKeyType();
887+
valueType = paramType.getContentType();
888+
prop = new BeanProperty.Std(PropertyName.construct(mutator.getName()),
889+
paramType, null, mutator, PropertyMetadata.STD_OPTIONAL);
890+
} else if (paramType.hasRawClass(JsonNode.class) || paramType.hasRawClass(ObjectNode.class)) {
891+
paramType = resolveMemberAndTypeAnnotations(ctxt, mutator, paramType);
892+
// Deserialize is individual values of ObjectNode, not full ObjectNode, so:
893+
valueType = ctxt.constructType(JsonNode.class);
894+
prop = new BeanProperty.Std(PropertyName.construct(mutator.getName()),
895+
paramType, null, mutator, PropertyMetadata.STD_OPTIONAL);
896+
897+
// Unlike with more complicated types, here we do not allow any annotation
898+
// overrides etc but instead short-cut handling:
899+
return SettableAnyProperty.constructForJsonNodeParameter(ctxt, prop, mutator, valueType,
900+
ctxt.findRootValueDeserializer(valueType), parameterIndex);
901+
} else {
902+
return ctxt.reportBadDefinition(beanDesc.getType(), String.format(
903+
"Unsupported type for any-setter: %s -- only support `Map`s, `JsonNode` and `ObjectNode` ",
904+
ClassUtil.getTypeDescription(paramType)));
905+
}
851906
} else {
852907
return ctxt.reportBadDefinition(beanDesc.getType(), String.format(
853908
"Unrecognized mutator type for any-setter: %s",
854909
ClassUtil.nameOf(mutator.getClass())));
855910
}
911+
912+
// NOTE: code from now on is for `Map` valued Any properties (JsonNode/ObjectNode
913+
// already returned; unsupported types threw Exception), if we have Field/Ctor-Parameter
914+
// any-setter -- or, basically Any supported type (if Method)
915+
856916
// First: see if there are explicitly specified
857917
// and then possible direct deserializer override on accessor
858918
KeyDeserializer keyDeser = findKeyDeserializerFromAnnotation(ctxt, mutator);
@@ -880,6 +940,10 @@ protected SettableAnyProperty constructAnySetter(DeserializationContext ctxt,
880940
return SettableAnyProperty.constructForMapField(ctxt,
881941
prop, mutator, valueType, keyDeser, deser, typeDeser);
882942
}
943+
if (isParameter) {
944+
return SettableAnyProperty.constructForMapParameter(ctxt,
945+
prop, mutator, valueType, keyDeser, deser, typeDeser, parameterIndex);
946+
}
883947
return SettableAnyProperty.constructForMethod(ctxt,
884948
prop, mutator, valueType, keyDeser, deser, typeDeser);
885949
}

src/main/java/com/fasterxml/jackson/databind/deser/SettableAnyProperty.java

Lines changed: 142 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,10 @@
11
package com.fasterxml.jackson.databind.deser;
22

33
import java.io.IOException;
4+
import java.util.HashMap;
45
import java.util.LinkedHashMap;
56
import java.util.Map;
7+
import java.util.Objects;
68

79
import com.fasterxml.jackson.core.*;
810
import com.fasterxml.jackson.databind.*;
@@ -117,6 +119,31 @@ public static SettableAnyProperty constructForJsonNodeField(DeserializationConte
117119
ctxt.getNodeFactory());
118120
}
119121

122+
/**
123+
* @since 2.18
124+
*/
125+
public static SettableAnyProperty constructForMapParameter(DeserializationContext ctxt,
126+
BeanProperty property, AnnotatedMember field, JavaType valueType, KeyDeserializer keyDeser,
127+
JsonDeserializer<Object> valueDeser, TypeDeserializer typeDeser, int parameterIndex
128+
) {
129+
Class<?> mapType = field.getRawType();
130+
// 02-Aug-2022, tatu: Ideally would be resolved to a concrete type by caller but
131+
// alas doesn't appear to happen. Nor does `BasicDeserializerFactory` expose method
132+
// for finding default or explicit mappings.
133+
if (mapType == Map.class) {
134+
mapType = LinkedHashMap.class;
135+
}
136+
ValueInstantiator vi = JDKValueInstantiators.findStdValueInstantiator(ctxt.getConfig(), mapType);
137+
return new MapParameterAnyProperty(property, field, valueType, keyDeser, valueDeser, typeDeser, vi, parameterIndex);
138+
}
139+
140+
public static SettableAnyProperty constructForJsonNodeParameter(DeserializationContext ctxt, BeanProperty prop,
141+
AnnotatedMember mutator, JavaType valueType, JsonDeserializer<Object> valueDeser, int parameterIndex
142+
) {
143+
return new JsonNodeParameterAnyProperty(prop, mutator, valueType, valueDeser, ctxt.getNodeFactory(), parameterIndex);
144+
}
145+
146+
120147
// Abstract @since 2.14
121148
public abstract SettableAnyProperty withValueDeserializer(JsonDeserializer<Object> deser);
122149

@@ -159,6 +186,23 @@ Object readResolve() {
159186
*/
160187
public String getPropertyName() { return _property.getName(); }
161188

189+
/**
190+
* Accessor for parameterIndex.
191+
* @return -1 if not a parameterized setter, otherwise index of parameter
192+
*
193+
* @since 2.18
194+
*/
195+
public int getParameterIndex() { return -1; }
196+
197+
/**
198+
* Create an instance of value to pass through Creator parameter.
199+
*
200+
* @since 2.18
201+
*/
202+
public Object createParameterObject() {
203+
throw new UnsupportedOperationException("Cannot call createParameterObject() on " + getClass().getName());
204+
}
205+
162206
/*
163207
/**********************************************************
164208
/* Public API, deserialization
@@ -437,4 +481,102 @@ public SettableAnyProperty withValueDeserializer(JsonDeserializer<Object> deser)
437481
return this;
438482
}
439483
}
484+
485+
486+
/**
487+
* [databind#562] Allow @JsonAnySetter on Creator constructor
488+
*
489+
* @since 2.18
490+
*/
491+
protected static class MapParameterAnyProperty extends SettableAnyProperty
492+
implements java.io.Serializable
493+
{
494+
private static final long serialVersionUID = 1L;
495+
496+
protected final ValueInstantiator _valueInstantiator;
497+
498+
protected final int _parameterIndex;
499+
500+
public MapParameterAnyProperty(BeanProperty property, AnnotatedMember field, JavaType valueType,
501+
KeyDeserializer keyDeser, JsonDeserializer<Object> valueDeser, TypeDeserializer typeDeser,
502+
ValueInstantiator inst, int parameterIndex)
503+
{
504+
super(property, field, valueType, keyDeser, valueDeser, typeDeser);
505+
_valueInstantiator = Objects.requireNonNull(inst, "ValueInstantiator for MapParameterAnyProperty cannot be `null`");
506+
_parameterIndex = parameterIndex;
507+
}
508+
509+
@Override
510+
public SettableAnyProperty withValueDeserializer(JsonDeserializer<Object> deser)
511+
{
512+
return new MapParameterAnyProperty(_property, _setter, _type, _keyDeserializer, deser,
513+
_valueTypeDeserializer, _valueInstantiator, _parameterIndex);
514+
}
515+
516+
@SuppressWarnings("unchecked")
517+
@Override
518+
protected void _set(Object instance, Object propName, Object value)
519+
{
520+
((Map<Object, Object>) instance).put(propName, value);
521+
}
522+
523+
@Override
524+
public int getParameterIndex() { return _parameterIndex; }
525+
526+
@Override
527+
public Object createParameterObject() { return new HashMap<>(); }
528+
529+
}
530+
531+
/**
532+
* [databind#562] Allow @JsonAnySetter on Creator constructor
533+
*
534+
* @since 2.18
535+
*/
536+
protected static class JsonNodeParameterAnyProperty extends SettableAnyProperty
537+
implements java.io.Serializable
538+
{
539+
private static final long serialVersionUID = 1L;
540+
541+
protected final JsonNodeFactory _nodeFactory;
542+
543+
protected final int _parameterIndex;
544+
545+
public JsonNodeParameterAnyProperty(BeanProperty property, AnnotatedMember field, JavaType valueType,
546+
JsonDeserializer<Object> valueDeser, JsonNodeFactory nodeFactory, int parameterIndex)
547+
{
548+
super(property, field, valueType, null, valueDeser, null);
549+
_nodeFactory = nodeFactory;
550+
_parameterIndex = parameterIndex;
551+
}
552+
553+
// Let's override since this is much simpler with JsonNodes
554+
@Override
555+
public Object deserialize(JsonParser p, DeserializationContext ctxt)
556+
throws IOException
557+
{
558+
return _valueDeserializer.deserialize(p, ctxt);
559+
}
560+
561+
@Override
562+
protected void _set(Object instance, Object propName, Object value)
563+
throws Exception
564+
{
565+
((ObjectNode) instance).set((String) propName, (JsonNode) value);
566+
}
567+
568+
// Should not get called but...
569+
@Override
570+
public SettableAnyProperty withValueDeserializer(JsonDeserializer<Object> deser) {
571+
throw new UnsupportedOperationException("Cannot call withValueDeserializer() on " + getClass().getName());
572+
}
573+
574+
@Override
575+
public int getParameterIndex() { return _parameterIndex; }
576+
577+
@Override
578+
public Object createParameterObject() { return _nodeFactory.objectNode(); }
579+
580+
}
581+
440582
}

src/main/java/com/fasterxml/jackson/databind/deser/impl/CreatorCollector.java

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -184,6 +184,7 @@ public void addPropertyCreator(AnnotatedWithParams creator,
184184
String name = properties[i].getName();
185185
// Need to consider Injectables, which may not have
186186
// a name at all, and need to be skipped
187+
// (same for possible AnySetter)
187188
if (name.isEmpty() && (properties[i].getInjectableValueId() != null)) {
188189
continue;
189190
}

0 commit comments

Comments
 (0)