Skip to content

Commit 7d8ee09

Browse files
committed
Implement #3394: allow JsonNode valued fields for @JsonAnySetter
1 parent 804f4e3 commit 7d8ee09

File tree

4 files changed

+223
-58
lines changed

4 files changed

+223
-58
lines changed

release-notes/VERSION-2.x

+2
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,8 @@ Project: jackson-databind
2020
(reported by lizongbo@github)
2121
#3373: Change `TypeSerializerBase` to skip `generator.writeTypePrefix()`
2222
for `null` typeId
23+
#3394: Allow use of `JsonNode` field for `@JsonAnySetter`
24+
(requested by @sixcorners)
2325
#3405: Create DataTypeFeature abstraction (for JSTEP-7) with placeholder features
2426
#3417: Allow (de)serializing records using Bean(De)SerializerModifier even when
2527
reflection is unavailable

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

+19-9
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@
1212
import com.fasterxml.jackson.databind.introspect.*;
1313
import com.fasterxml.jackson.databind.jsontype.TypeDeserializer;
1414
import com.fasterxml.jackson.databind.jsontype.impl.SubTypeValidator;
15+
import com.fasterxml.jackson.databind.node.ObjectNode;
1516
import com.fasterxml.jackson.databind.util.BeanUtil;
1617
import com.fasterxml.jackson.databind.util.ClassUtil;
1718
import com.fasterxml.jackson.databind.util.IgnorePropertiesUtil;
@@ -806,6 +807,7 @@ protected SettableAnyProperty constructAnySetter(DeserializationContext ctxt,
806807
BeanProperty prop;
807808
JavaType keyType;
808809
JavaType valueType;
810+
final boolean isField = mutator instanceof AnnotatedField;
809811

810812
if (mutator instanceof AnnotatedMethod) {
811813
// we know it's a 2-arg method, second arg is the value
@@ -817,7 +819,7 @@ protected SettableAnyProperty constructAnySetter(DeserializationContext ctxt,
817819
valueType, null, mutator,
818820
PropertyMetadata.STD_OPTIONAL);
819821

820-
} else if (mutator instanceof AnnotatedField) {
822+
} else if (isField) {
821823
AnnotatedField af = (AnnotatedField) mutator;
822824
// get the type from the content type of the map object
823825
JavaType fieldType = af.getType();
@@ -828,24 +830,28 @@ protected SettableAnyProperty constructAnySetter(DeserializationContext ctxt,
828830
valueType = fieldType.getContentType();
829831
prop = new BeanProperty.Std(PropertyName.construct(mutator.getName()),
830832
fieldType, null, mutator, PropertyMetadata.STD_OPTIONAL);
831-
} else if (fieldType.isTypeOrSubTypeOf(JsonNode.class)) {
833+
} else if (fieldType.hasRawClass(JsonNode.class)
834+
|| fieldType.hasRawClass(ObjectNode.class)) {
832835
fieldType = resolveMemberAndTypeAnnotations(ctxt, mutator, fieldType);
833-
keyType = null; // so it's plain old `String`
836+
// Deserialize is individual values of ObjectNode, not full ObjectNode, so:
834837
valueType = ctxt.constructType(JsonNode.class);
835838
prop = new BeanProperty.Std(PropertyName.construct(mutator.getName()),
836839
fieldType, null, mutator, PropertyMetadata.STD_OPTIONAL);
837840

838841
// Unlike with more complicated types, here we do not allow any annotation
839842
// overrides etc but instead short-cut handling:
840-
return new SettableAnyProperty(prop, mutator, valueType,
841-
null, null, null);
843+
return SettableAnyProperty.constructForJsonNodeField(ctxt,
844+
prop, mutator, valueType,
845+
ctxt.findRootValueDeserializer(valueType));
842846
} else {
843847
return ctxt.reportBadDefinition(beanDesc.getType(), String.format(
844-
"Unsupported type for any-setter: %s", ClassUtil.getTypeDescription(fieldType)));
848+
"Unsupported type for any-setter: %s -- only support `Map`s, `JsonNode` and `ObjectNode` ",
849+
ClassUtil.getTypeDescription(fieldType)));
845850
}
846851
} else {
847852
return ctxt.reportBadDefinition(beanDesc.getType(), String.format(
848-
"Unrecognized mutator type for any-setter: %s", ClassUtil.nameOf(mutator.getClass())));
853+
"Unrecognized mutator type for any-setter: %s",
854+
ClassUtil.nameOf(mutator.getClass())));
849855
}
850856
// First: see if there are explicitly specified
851857
// and then possible direct deserializer override on accessor
@@ -870,8 +876,12 @@ protected SettableAnyProperty constructAnySetter(DeserializationContext ctxt,
870876
deser = (JsonDeserializer<Object>) ctxt.handlePrimaryContextualization(deser, prop, valueType);
871877
}
872878
TypeDeserializer typeDeser = valueType.getTypeHandler();
873-
return new SettableAnyProperty(prop, mutator, valueType,
874-
keyDeser, deser, typeDeser);
879+
if (isField) {
880+
return SettableAnyProperty.constructForMapField(ctxt,
881+
prop, mutator, valueType, keyDeser, deser, typeDeser);
882+
}
883+
return SettableAnyProperty.constructForMethod(ctxt,
884+
prop, mutator, valueType, keyDeser, deser, typeDeser);
875885
}
876886

877887
/**

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

+200-43
Original file line numberDiff line numberDiff line change
@@ -12,17 +12,19 @@
1212
import com.fasterxml.jackson.databind.introspect.AnnotatedMember;
1313
import com.fasterxml.jackson.databind.introspect.AnnotatedMethod;
1414
import com.fasterxml.jackson.databind.jsontype.TypeDeserializer;
15+
import com.fasterxml.jackson.databind.node.JsonNodeFactory;
16+
import com.fasterxml.jackson.databind.node.ObjectNode;
1517
import com.fasterxml.jackson.databind.util.ClassUtil;
1618

1719
/**
1820
* Class that represents a "wildcard" set method which can be used
1921
* to generically set values of otherwise unmapped (aka "unknown")
2022
* properties read from JSON content.
2123
*<p>
22-
* !!! Note: might make sense to refactor to share some code
23-
* with {@link SettableBeanProperty}?
24+
* Note: starting with 2.14, is {@code abstract} class with multiple
25+
* concrete implementations
2426
*/
25-
public class SettableAnyProperty
27+
public abstract class SettableAnyProperty
2628
implements java.io.Serializable
2729
{
2830
private static final long serialVersionUID = 1L;
@@ -70,11 +72,54 @@ public SettableAnyProperty(BeanProperty property, AnnotatedMember setter, JavaTy
7072
_setterIsField = setter instanceof AnnotatedField;
7173
}
7274

73-
public SettableAnyProperty withValueDeserializer(JsonDeserializer<Object> deser) {
74-
return new SettableAnyProperty(_property, _setter, _type,
75-
_keyDeserializer, deser, _valueTypeDeserializer);
75+
/**
76+
* @since 2.14
77+
*/
78+
public static SettableAnyProperty constructForMethod(DeserializationContext ctxt,
79+
BeanProperty property,
80+
AnnotatedMember field, JavaType valueType,
81+
KeyDeserializer keyDeser,
82+
JsonDeserializer<Object> valueDeser, TypeDeserializer typeDeser) {
83+
return new MethodAnyProperty(property, field, valueType,
84+
keyDeser, valueDeser, typeDeser);
85+
}
86+
87+
/**
88+
* @since 2.14
89+
*/
90+
public static SettableAnyProperty constructForMapField(DeserializationContext ctxt,
91+
BeanProperty property,
92+
AnnotatedMember field, JavaType valueType,
93+
KeyDeserializer keyDeser,
94+
JsonDeserializer<Object> valueDeser, TypeDeserializer typeDeser)
95+
{
96+
Class<?> mapType = field.getRawType();
97+
// 02-Aug-2022, tatu: Ideally would be resolved to a concrete type by caller but
98+
// alas doesn't appear to happen. Nor does `BasicDeserializerFactory` expose method
99+
// for finding default or explicit mappings.
100+
if (mapType == Map.class) {
101+
mapType = LinkedHashMap.class;
102+
}
103+
ValueInstantiator vi = JDKValueInstantiators.findStdValueInstantiator(ctxt.getConfig(), mapType);
104+
return new MapFieldAnyProperty(property, field, valueType,
105+
keyDeser, valueDeser, typeDeser,
106+
vi);
76107
}
77108

109+
/**
110+
* @since 2.14
111+
*/
112+
public static SettableAnyProperty constructForJsonNodeField(DeserializationContext ctxt,
113+
BeanProperty property,
114+
AnnotatedMember field, JavaType valueType, JsonDeserializer<Object> valueDeser) {
115+
return new JsonNodeFieldAnyProperty(property, field, valueType,
116+
valueDeser,
117+
ctxt.getNodeFactory());
118+
}
119+
120+
// Abstract @since 2.14
121+
public abstract SettableAnyProperty withValueDeserializer(JsonDeserializer<Object> deser);
122+
78123
public void fixAccess(DeserializationConfig config) {
79124
_setter.fixAccess(
80125
config.isEnabled(MapperFeature.OVERRIDE_PUBLIC_ACCESS_MODIFIERS));
@@ -109,6 +154,11 @@ Object readResolve() {
109154

110155
public JavaType getType() { return _type; }
111156

157+
/**
158+
* @since 2.14
159+
*/
160+
public String getPropertyName() { return _property.getName(); }
161+
112162
/*
113163
/**********************************************************
114164
/* Public API, deserialization
@@ -148,53 +198,19 @@ public Object deserialize(JsonParser p, DeserializationContext ctxt) throws IOEx
148198
return _valueDeserializer.deserialize(p, ctxt);
149199
}
150200

151-
@SuppressWarnings("unchecked")
201+
// Default implementation since 2.14
152202
public void set(Object instance, Object propName, Object value) throws IOException
153203
{
154204
try {
155-
// if annotation in the field (only map is supported now)
156-
if (_setterIsField) {
157-
AnnotatedField field = (AnnotatedField) _setter;
158-
Map<Object,Object> val = (Map<Object,Object>) field.getValue(instance);
159-
// 01-Aug-2022, tatu: [databind#3559] Will try to create and assign an
160-
// instance.
161-
if (val == null) {
162-
val = _createAndSetMap(null, field, instance, propName);
163-
}
164-
// add the property key and value
165-
val.put(propName, value);
166-
} else {
167-
// note: cannot use 'setValue()' due to taking 2 args
168-
((AnnotatedMethod) _setter).callOnWith(instance, propName, value);
169-
}
205+
_set(instance, propName, value);
170206
} catch (IOException e) {
171207
throw e;
172208
} catch (Exception e) {
173209
_throwAsIOE(e, propName, value);
174210
}
175211
}
176212

177-
@SuppressWarnings("unchecked")
178-
protected Map<Object, Object> _createAndSetMap(DeserializationContext ctxt, AnnotatedField field,
179-
Object instance, Object propName)
180-
throws IOException
181-
{
182-
Class<?> mapType = field.getRawType();
183-
// Ideally would be resolved to a concrete type but if not:
184-
if (mapType == Map.class) {
185-
mapType = LinkedHashMap.class;
186-
}
187-
// We know that DeserializationContext not actually required:
188-
ValueInstantiator vi = JDKValueInstantiators.findStdValueInstantiator(null, mapType);
189-
if (vi == null) {
190-
throw JsonMappingException.from(ctxt, String.format(
191-
"Cannot create an instance of %s for use as \"any-setter\" '%s'",
192-
ClassUtil.nameOf(mapType), _property.getName()));
193-
}
194-
Map<Object,Object> map = (Map<Object,Object>) vi.createUsingDefault(ctxt);
195-
field.setValue(instance, map);
196-
return map;
197-
}
213+
protected abstract void _set(Object instance, Object propName, Object value) throws Exception;
198214

199215
/*
200216
/**********************************************************
@@ -259,4 +275,145 @@ public void handleResolvedForwardReference(Object id, Object value)
259275
_parent.set(_pojo, _propName, value);
260276
}
261277
}
278+
279+
/*
280+
/**********************************************************************
281+
/* Concrete implementations
282+
/**********************************************************************
283+
*/
284+
285+
/**
286+
* @since 2.14
287+
*/
288+
protected static class MethodAnyProperty extends SettableAnyProperty
289+
implements java.io.Serializable
290+
{
291+
private static final long serialVersionUID = 1L;
292+
293+
public MethodAnyProperty(BeanProperty property,
294+
AnnotatedMember field, JavaType valueType,
295+
KeyDeserializer keyDeser,
296+
JsonDeserializer<Object> valueDeser, TypeDeserializer typeDeser) {
297+
super(property, field, valueType,
298+
keyDeser, valueDeser, typeDeser);
299+
}
300+
301+
@Override
302+
protected void _set(Object instance, Object propName, Object value) throws Exception
303+
{
304+
// note: cannot use 'setValue()' due to taking 2 args
305+
((AnnotatedMethod) _setter).callOnWith(instance, propName, value);
306+
}
307+
308+
@Override
309+
public SettableAnyProperty withValueDeserializer(JsonDeserializer<Object> deser) {
310+
return new MethodAnyProperty(_property, _setter, _type,
311+
_keyDeserializer, deser, _valueTypeDeserializer);
312+
}
313+
}
314+
315+
/**
316+
* @since 2.14
317+
*/
318+
protected static class MapFieldAnyProperty extends SettableAnyProperty
319+
implements java.io.Serializable
320+
{
321+
private static final long serialVersionUID = 1L;
322+
323+
protected final ValueInstantiator _valueInstantiator;
324+
325+
public MapFieldAnyProperty(BeanProperty property,
326+
AnnotatedMember field, JavaType valueType,
327+
KeyDeserializer keyDeser,
328+
JsonDeserializer<Object> valueDeser, TypeDeserializer typeDeser,
329+
ValueInstantiator inst) {
330+
super(property, field, valueType,
331+
keyDeser, valueDeser, typeDeser);
332+
_valueInstantiator = inst;
333+
}
334+
335+
@Override
336+
public SettableAnyProperty withValueDeserializer(JsonDeserializer<Object> deser) {
337+
return new MapFieldAnyProperty(_property, _setter, _type,
338+
_keyDeserializer, deser, _valueTypeDeserializer,
339+
_valueInstantiator);
340+
}
341+
342+
@SuppressWarnings("unchecked")
343+
@Override
344+
protected void _set(Object instance, Object propName, Object value) throws Exception
345+
{
346+
AnnotatedField field = (AnnotatedField) _setter;
347+
Map<Object,Object> val = (Map<Object,Object>) field.getValue(instance);
348+
// 01-Aug-2022, tatu: [databind#3559] Will try to create and assign an
349+
// instance.
350+
if (val == null) {
351+
val = _createAndSetMap(null, field, instance, propName);
352+
}
353+
// add the property key and value
354+
val.put(propName, value);
355+
}
356+
357+
@SuppressWarnings("unchecked")
358+
protected Map<Object, Object> _createAndSetMap(DeserializationContext ctxt, AnnotatedField field,
359+
Object instance, Object propName)
360+
throws IOException
361+
{
362+
if (_valueInstantiator == null) {
363+
throw JsonMappingException.from(ctxt, String.format(
364+
"Cannot create an instance of %s for use as \"any-setter\" '%s'",
365+
ClassUtil.nameOf(_type.getRawClass()), _property.getName()));
366+
}
367+
Map<Object,Object> map = (Map<Object,Object>) _valueInstantiator.createUsingDefault(ctxt);
368+
field.setValue(instance, map);
369+
return map;
370+
}
371+
}
372+
373+
/**
374+
* @since 2.14
375+
*/
376+
protected static class JsonNodeFieldAnyProperty extends SettableAnyProperty
377+
implements java.io.Serializable
378+
{
379+
private static final long serialVersionUID = 1L;
380+
381+
protected final JsonNodeFactory _nodeFactory;
382+
383+
public JsonNodeFieldAnyProperty(BeanProperty property,
384+
AnnotatedMember field, JavaType valueType,
385+
JsonDeserializer<Object> valueDeser,
386+
JsonNodeFactory nodeFactory) {
387+
super(property, field, valueType, null, valueDeser, null);
388+
_nodeFactory = nodeFactory;
389+
}
390+
391+
@Override
392+
protected void _set(Object instance, Object propName, Object value) throws Exception
393+
{
394+
AnnotatedField field = (AnnotatedField) _setter;
395+
Object val0 = field.getValue(instance);
396+
ObjectNode objectNode;
397+
398+
if (val0 == null) {
399+
objectNode = _nodeFactory.objectNode();
400+
field.setValue(instance, objectNode);
401+
} else if (!(val0 instanceof ObjectNode)) {
402+
throw JsonMappingException.from((DeserializationContext) null, String.format(
403+
"Value \"any-setter\" '%s' not `ObjectNode` but %s",
404+
getPropertyName(),
405+
ClassUtil.nameOf(val0.getClass())));
406+
} else {
407+
objectNode = (ObjectNode) val0;
408+
}
409+
// add the property key and value
410+
objectNode.set(String.valueOf(propName), (JsonNode) value);
411+
}
412+
413+
// Should not get called but...
414+
@Override
415+
public SettableAnyProperty withValueDeserializer(JsonDeserializer<Object> deser) {
416+
return this;
417+
}
418+
}
262419
}

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

+2-6
Original file line numberDiff line numberDiff line change
@@ -1,14 +1,10 @@
11
package com.fasterxml.jackson.databind.deser.impl;
22

33
import java.io.IOException;
4-
import java.util.ArrayList;
5-
import java.util.Collection;
6-
import java.util.Collections;
7-
import java.util.HashMap;
8-
import java.util.LinkedHashMap;
9-
import java.util.Map;
4+
import java.util.*;
105

116
import com.fasterxml.jackson.core.JsonLocation;
7+
128
import com.fasterxml.jackson.databind.DeserializationConfig;
139
import com.fasterxml.jackson.databind.DeserializationContext;
1410
import com.fasterxml.jackson.databind.deser.ValueInstantiator;

0 commit comments

Comments
 (0)