Skip to content

Commit

Permalink
Implement #3394: allow JsonNode valued fields for @JsonAnySetter
Browse files Browse the repository at this point in the history
  • Loading branch information
cowtowncoder committed Aug 2, 2022
1 parent 804f4e3 commit 7d8ee09
Show file tree
Hide file tree
Showing 4 changed files with 223 additions and 58 deletions.
2 changes: 2 additions & 0 deletions release-notes/VERSION-2.x
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,8 @@ Project: jackson-databind
(reported by lizongbo@github)
#3373: Change `TypeSerializerBase` to skip `generator.writeTypePrefix()`
for `null` typeId
#3394: Allow use of `JsonNode` field for `@JsonAnySetter`
(requested by @sixcorners)
#3405: Create DataTypeFeature abstraction (for JSTEP-7) with placeholder features
#3417: Allow (de)serializing records using Bean(De)SerializerModifier even when
reflection is unavailable
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@
import com.fasterxml.jackson.databind.introspect.*;
import com.fasterxml.jackson.databind.jsontype.TypeDeserializer;
import com.fasterxml.jackson.databind.jsontype.impl.SubTypeValidator;
import com.fasterxml.jackson.databind.node.ObjectNode;
import com.fasterxml.jackson.databind.util.BeanUtil;
import com.fasterxml.jackson.databind.util.ClassUtil;
import com.fasterxml.jackson.databind.util.IgnorePropertiesUtil;
Expand Down Expand Up @@ -806,6 +807,7 @@ protected SettableAnyProperty constructAnySetter(DeserializationContext ctxt,
BeanProperty prop;
JavaType keyType;
JavaType valueType;
final boolean isField = mutator instanceof AnnotatedField;

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

} else if (mutator instanceof AnnotatedField) {
} else if (isField) {
AnnotatedField af = (AnnotatedField) mutator;
// get the type from the content type of the map object
JavaType fieldType = af.getType();
Expand All @@ -828,24 +830,28 @@ protected SettableAnyProperty constructAnySetter(DeserializationContext ctxt,
valueType = fieldType.getContentType();
prop = new BeanProperty.Std(PropertyName.construct(mutator.getName()),
fieldType, null, mutator, PropertyMetadata.STD_OPTIONAL);
} else if (fieldType.isTypeOrSubTypeOf(JsonNode.class)) {
} else if (fieldType.hasRawClass(JsonNode.class)
|| fieldType.hasRawClass(ObjectNode.class)) {
fieldType = resolveMemberAndTypeAnnotations(ctxt, mutator, fieldType);
keyType = null; // so it's plain old `String`
// 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 new SettableAnyProperty(prop, mutator, valueType,
null, null, null);
return SettableAnyProperty.constructForJsonNodeField(ctxt,
prop, mutator, valueType,
ctxt.findRootValueDeserializer(valueType));
} else {
return ctxt.reportBadDefinition(beanDesc.getType(), String.format(
"Unsupported type for any-setter: %s", ClassUtil.getTypeDescription(fieldType)));
"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())));
"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
Expand All @@ -870,8 +876,12 @@ protected SettableAnyProperty constructAnySetter(DeserializationContext ctxt,
deser = (JsonDeserializer<Object>) ctxt.handlePrimaryContextualization(deser, prop, valueType);
}
TypeDeserializer typeDeser = valueType.getTypeHandler();
return new SettableAnyProperty(prop, mutator, valueType,
keyDeser, deser, typeDeser);
if (isField) {
return SettableAnyProperty.constructForMapField(ctxt,
prop, mutator, valueType, keyDeser, deser, typeDeser);
}
return SettableAnyProperty.constructForMethod(ctxt,
prop, mutator, valueType, keyDeser, deser, typeDeser);
}

/**
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -12,17 +12,19 @@
import com.fasterxml.jackson.databind.introspect.AnnotatedMember;
import com.fasterxml.jackson.databind.introspect.AnnotatedMethod;
import com.fasterxml.jackson.databind.jsontype.TypeDeserializer;
import com.fasterxml.jackson.databind.node.JsonNodeFactory;
import com.fasterxml.jackson.databind.node.ObjectNode;
import com.fasterxml.jackson.databind.util.ClassUtil;

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

public SettableAnyProperty withValueDeserializer(JsonDeserializer<Object> deser) {
return new SettableAnyProperty(_property, _setter, _type,
_keyDeserializer, deser, _valueTypeDeserializer);
/**
* @since 2.14
*/
public static SettableAnyProperty constructForMethod(DeserializationContext ctxt,
BeanProperty property,
AnnotatedMember field, JavaType valueType,
KeyDeserializer keyDeser,
JsonDeserializer<Object> valueDeser, TypeDeserializer typeDeser) {
return new MethodAnyProperty(property, field, valueType,
keyDeser, valueDeser, typeDeser);
}

/**
* @since 2.14
*/
public static SettableAnyProperty constructForMapField(DeserializationContext ctxt,
BeanProperty property,
AnnotatedMember field, JavaType valueType,
KeyDeserializer keyDeser,
JsonDeserializer<Object> 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 MapFieldAnyProperty(property, field, valueType,
keyDeser, valueDeser, typeDeser,
vi);
}

/**
* @since 2.14
*/
public static SettableAnyProperty constructForJsonNodeField(DeserializationContext ctxt,
BeanProperty property,
AnnotatedMember field, JavaType valueType, JsonDeserializer<Object> valueDeser) {
return new JsonNodeFieldAnyProperty(property, field, valueType,
valueDeser,
ctxt.getNodeFactory());
}

// Abstract @since 2.14
public abstract SettableAnyProperty withValueDeserializer(JsonDeserializer<Object> deser);

public void fixAccess(DeserializationConfig config) {
_setter.fixAccess(
config.isEnabled(MapperFeature.OVERRIDE_PUBLIC_ACCESS_MODIFIERS));
Expand Down Expand Up @@ -109,6 +154,11 @@ Object readResolve() {

public JavaType getType() { return _type; }

/**
* @since 2.14
*/
public String getPropertyName() { return _property.getName(); }

/*
/**********************************************************
/* Public API, deserialization
Expand Down Expand Up @@ -148,53 +198,19 @@ public Object deserialize(JsonParser p, DeserializationContext ctxt) throws IOEx
return _valueDeserializer.deserialize(p, ctxt);
}

@SuppressWarnings("unchecked")
// Default implementation since 2.14
public void set(Object instance, Object propName, Object value) throws IOException
{
try {
// if annotation in the field (only map is supported now)
if (_setterIsField) {
AnnotatedField field = (AnnotatedField) _setter;
Map<Object,Object> val = (Map<Object,Object>) field.getValue(instance);
// 01-Aug-2022, tatu: [databind#3559] Will try to create and assign an
// instance.
if (val == null) {
val = _createAndSetMap(null, field, instance, propName);
}
// add the property key and value
val.put(propName, value);
} else {
// note: cannot use 'setValue()' due to taking 2 args
((AnnotatedMethod) _setter).callOnWith(instance, propName, value);
}
_set(instance, propName, value);
} catch (IOException e) {
throw e;
} catch (Exception e) {
_throwAsIOE(e, propName, value);
}
}

@SuppressWarnings("unchecked")
protected Map<Object, Object> _createAndSetMap(DeserializationContext ctxt, AnnotatedField field,
Object instance, Object propName)
throws IOException
{
Class<?> mapType = field.getRawType();
// Ideally would be resolved to a concrete type but if not:
if (mapType == Map.class) {
mapType = LinkedHashMap.class;
}
// We know that DeserializationContext not actually required:
ValueInstantiator vi = JDKValueInstantiators.findStdValueInstantiator(null, mapType);
if (vi == null) {
throw JsonMappingException.from(ctxt, String.format(
"Cannot create an instance of %s for use as \"any-setter\" '%s'",
ClassUtil.nameOf(mapType), _property.getName()));
}
Map<Object,Object> map = (Map<Object,Object>) vi.createUsingDefault(ctxt);
field.setValue(instance, map);
return map;
}
protected abstract void _set(Object instance, Object propName, Object value) throws Exception;

/*
/**********************************************************
Expand Down Expand Up @@ -259,4 +275,145 @@ public void handleResolvedForwardReference(Object id, Object value)
_parent.set(_pojo, _propName, value);
}
}

/*
/**********************************************************************
/* Concrete implementations
/**********************************************************************
*/

/**
* @since 2.14
*/
protected static class MethodAnyProperty extends SettableAnyProperty
implements java.io.Serializable
{
private static final long serialVersionUID = 1L;

public MethodAnyProperty(BeanProperty property,
AnnotatedMember field, JavaType valueType,
KeyDeserializer keyDeser,
JsonDeserializer<Object> valueDeser, TypeDeserializer typeDeser) {
super(property, field, valueType,
keyDeser, valueDeser, typeDeser);
}

@Override
protected void _set(Object instance, Object propName, Object value) throws Exception
{
// note: cannot use 'setValue()' due to taking 2 args
((AnnotatedMethod) _setter).callOnWith(instance, propName, value);
}

@Override
public SettableAnyProperty withValueDeserializer(JsonDeserializer<Object> deser) {
return new MethodAnyProperty(_property, _setter, _type,
_keyDeserializer, deser, _valueTypeDeserializer);
}
}

/**
* @since 2.14
*/
protected static class MapFieldAnyProperty extends SettableAnyProperty
implements java.io.Serializable
{
private static final long serialVersionUID = 1L;

protected final ValueInstantiator _valueInstantiator;

public MapFieldAnyProperty(BeanProperty property,
AnnotatedMember field, JavaType valueType,
KeyDeserializer keyDeser,
JsonDeserializer<Object> valueDeser, TypeDeserializer typeDeser,
ValueInstantiator inst) {
super(property, field, valueType,
keyDeser, valueDeser, typeDeser);
_valueInstantiator = inst;
}

@Override
public SettableAnyProperty withValueDeserializer(JsonDeserializer<Object> deser) {
return new MapFieldAnyProperty(_property, _setter, _type,
_keyDeserializer, deser, _valueTypeDeserializer,
_valueInstantiator);
}

@SuppressWarnings("unchecked")
@Override
protected void _set(Object instance, Object propName, Object value) throws Exception
{
AnnotatedField field = (AnnotatedField) _setter;
Map<Object,Object> val = (Map<Object,Object>) field.getValue(instance);
// 01-Aug-2022, tatu: [databind#3559] Will try to create and assign an
// instance.
if (val == null) {
val = _createAndSetMap(null, field, instance, propName);
}
// add the property key and value
val.put(propName, value);
}

@SuppressWarnings("unchecked")
protected Map<Object, Object> _createAndSetMap(DeserializationContext ctxt, AnnotatedField field,
Object instance, Object propName)
throws IOException
{
if (_valueInstantiator == null) {
throw JsonMappingException.from(ctxt, String.format(
"Cannot create an instance of %s for use as \"any-setter\" '%s'",
ClassUtil.nameOf(_type.getRawClass()), _property.getName()));
}
Map<Object,Object> map = (Map<Object,Object>) _valueInstantiator.createUsingDefault(ctxt);
field.setValue(instance, map);
return map;
}
}

/**
* @since 2.14
*/
protected static class JsonNodeFieldAnyProperty extends SettableAnyProperty
implements java.io.Serializable
{
private static final long serialVersionUID = 1L;

protected final JsonNodeFactory _nodeFactory;

public JsonNodeFieldAnyProperty(BeanProperty property,
AnnotatedMember field, JavaType valueType,
JsonDeserializer<Object> valueDeser,
JsonNodeFactory nodeFactory) {
super(property, field, valueType, null, valueDeser, null);
_nodeFactory = nodeFactory;
}

@Override
protected void _set(Object instance, Object propName, Object value) throws Exception
{
AnnotatedField field = (AnnotatedField) _setter;
Object val0 = field.getValue(instance);
ObjectNode objectNode;

if (val0 == null) {
objectNode = _nodeFactory.objectNode();
field.setValue(instance, objectNode);
} else if (!(val0 instanceof ObjectNode)) {
throw JsonMappingException.from((DeserializationContext) null, String.format(
"Value \"any-setter\" '%s' not `ObjectNode` but %s",
getPropertyName(),
ClassUtil.nameOf(val0.getClass())));
} else {
objectNode = (ObjectNode) val0;
}
// add the property key and value
objectNode.set(String.valueOf(propName), (JsonNode) value);
}

// Should not get called but...
@Override
public SettableAnyProperty withValueDeserializer(JsonDeserializer<Object> deser) {
return this;
}
}
}
Original file line number Diff line number Diff line change
@@ -1,14 +1,10 @@
package com.fasterxml.jackson.databind.deser.impl;

import java.io.IOException;
import java.util.ArrayList;
import java.util.Collection;
import java.util.Collections;
import java.util.HashMap;
import java.util.LinkedHashMap;
import java.util.Map;
import java.util.*;

import com.fasterxml.jackson.core.JsonLocation;

import com.fasterxml.jackson.databind.DeserializationConfig;
import com.fasterxml.jackson.databind.DeserializationContext;
import com.fasterxml.jackson.databind.deser.ValueInstantiator;
Expand Down

0 comments on commit 7d8ee09

Please sign in to comment.