Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Allow @JsonAnySetter on Creator constructors #4366

Closed
wants to merge 72 commits into from
Closed
Show file tree
Hide file tree
Changes from 23 commits
Commits
Show all changes
72 commits
Select commit Hold shift + click to select a range
94540f8
temporarily remove other jdk versions
JooHyukKim Jan 27, 2024
9c6cace
Merge branch '2.17' into 562-jsonanysetter-test
JooHyukKim Jan 28, 2024
1faccf0
Working solution first draft
JooHyukKim Feb 3, 2024
8930a4e
Revert "temporarily remove other jdk versions"
JooHyukKim Feb 3, 2024
5280004
Delete JsonAnySetterThroughCreator562Test.java
JooHyukKim Feb 3, 2024
99a882b
Remove hard coded field names
JooHyukKim Feb 3, 2024
a840e56
Clean Up
JooHyukKim Feb 3, 2024
75c23f1
Clean up unused _anySetter
JooHyukKim Feb 3, 2024
2b7d22f
Add back required property _anySetter implementation
JooHyukKim Feb 3, 2024
2f49175
Merge branch '2.17' into 562-jsonanysetter-test
JooHyukKim Feb 3, 2024
1a9afab
Add `Boolean _ hasAnySetter` in
JooHyukKim Feb 4, 2024
2b2e0aa
Use hasAnySetter from CreatorProperty
JooHyukKim Feb 4, 2024
6a892f8
Remove unncessary anySetter construction
JooHyukKim Feb 4, 2024
765ed3a
Revert "Remove unncessary anySetter construction"
JooHyukKim Feb 4, 2024
57d467a
Put back isAnySetterXxx methods and fields
JooHyukKim Feb 4, 2024
2ae2dca
Fix JDK 8 error
JooHyukKim Feb 4, 2024
c4657cc
Merge branch '2.17' into 562-jsonanysetter-test
JooHyukKim Feb 9, 2024
7da980a
Move test dir
JooHyukKim Feb 9, 2024
09d4c82
Fix error
JooHyukKim Feb 9, 2024
9baad77
Add new SettableAnyProperty impl
JooHyukKim Feb 9, 2024
81a8803
Remove hard coded prop name
JooHyukKim Feb 9, 2024
742a589
Update javadoc and clean comments
JooHyukKim Feb 9, 2024
19182fa
Merge branch '2.17' into 562-jsonanysetter-test
cowtowncoder Feb 10, 2024
010849a
Minor clean up, streamlining
cowtowncoder Feb 10, 2024
10540a8
Add verification there's only one any-setter
cowtowncoder Feb 10, 2024
1458882
Test case for disabled(@JsonAnySetter) (rain check?
JooHyukKim Feb 10, 2024
bddb042
Require non-null _valueInstantiator during construction
JooHyukKim Feb 10, 2024
5c1f30d
Add test with DeserializationFeature.FAIL_ON_NULL_CREATOR_PROPERTIES
JooHyukKim Feb 10, 2024
bdc2357
Fix tests
JooHyukKim Feb 10, 2024
47ac3f7
Update AnySetterForCreator562Test.java
JooHyukKim Feb 10, 2024
8612433
Implement version 2
JooHyukKim Feb 10, 2024
5d69465
Clean up changes
JooHyukKim Feb 10, 2024
551a001
Merge branch '2.17' into 562-jsonanysetter-test
JooHyukKim Feb 12, 2024
7d7a4c5
Merge branch '2.17' into 562-jsonanysetter-test
cowtowncoder Feb 19, 2024
778ddc3
Merge branch '2.17' into 562-jsonanysetter-test
JooHyukKim Apr 8, 2024
30c0f57
Merge branch '2.17' into 562-jsonanysetter-test
cowtowncoder May 31, 2024
7a55942
temporarily remove other jdk versions
JooHyukKim Jan 27, 2024
ff22500
Working solution first draft
JooHyukKim Feb 3, 2024
e2670e6
Revert "temporarily remove other jdk versions"
JooHyukKim Feb 3, 2024
05a4c0b
Delete JsonAnySetterThroughCreator562Test.java
JooHyukKim Feb 3, 2024
dd53092
Remove hard coded field names
JooHyukKim Feb 3, 2024
49f8b95
Clean Up
JooHyukKim Feb 3, 2024
b4d03e3
Clean up unused _anySetter
JooHyukKim Feb 3, 2024
426f303
Add back required property _anySetter implementation
JooHyukKim Feb 3, 2024
201c5ea
Add `Boolean _ hasAnySetter` in
JooHyukKim Feb 4, 2024
a247d02
Use hasAnySetter from CreatorProperty
JooHyukKim Feb 4, 2024
2ece9f9
Remove unncessary anySetter construction
JooHyukKim Feb 4, 2024
d3d8546
Revert "Remove unncessary anySetter construction"
JooHyukKim Feb 4, 2024
3d632f4
Put back isAnySetterXxx methods and fields
JooHyukKim Feb 4, 2024
ce58b14
Fix JDK 8 error
JooHyukKim Feb 4, 2024
21a0795
Move test dir
JooHyukKim Feb 9, 2024
09296d1
Fix error
JooHyukKim Feb 9, 2024
3445ddd
Add new SettableAnyProperty impl
JooHyukKim Feb 9, 2024
9e95155
Remove hard coded prop name
JooHyukKim Feb 9, 2024
1908532
Update javadoc and clean comments
JooHyukKim Feb 9, 2024
659bbf2
Minor clean up, streamlining
cowtowncoder Feb 10, 2024
fcfac63
Add verification there's only one any-setter
cowtowncoder Feb 10, 2024
f6a6c37
Test case for disabled(@JsonAnySetter) (rain check?
JooHyukKim Feb 10, 2024
93b8587
Require non-null _valueInstantiator during construction
JooHyukKim Feb 10, 2024
134d8d1
Add test with DeserializationFeature.FAIL_ON_NULL_CREATOR_PROPERTIES
JooHyukKim Feb 10, 2024
7c60905
Fix tests
JooHyukKim Feb 10, 2024
3e62a2b
Update AnySetterForCreator562Test.java
JooHyukKim Feb 10, 2024
60a2933
Implement version 2
JooHyukKim Feb 10, 2024
40061f9
Clean up changes
JooHyukKim Feb 10, 2024
cd86a74
Add missing imports
JooHyukKim May 31, 2024
6cbdeff
Merge branch '562-jsonanysetter-test' of https://github.com/JooHyukKi…
JooHyukKim May 31, 2024
8046794
Merge branch '2.18' into 562-jsonanysetter-test
cowtowncoder May 31, 2024
2de9beb
Clean up code, version, etc...
JooHyukKim May 31, 2024
825f31e
Merge branch '2.18' into 562-jsonanysetter-test
cowtowncoder May 31, 2024
cdcdf3e
Merge branch '2.18' into 562-jsonanysetter-test
cowtowncoder May 31, 2024
2cd2b9d
Apply review
JooHyukKim Jun 1, 2024
b616cb6
Change initMap() method to createAnyPropertyMap()
JooHyukKim Jun 1, 2024
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -867,6 +867,8 @@ protected void _addExplicitPropertyCreator(DeserializationContext ctxt,
JacksonInject.Value injectId = candidate.injection(i);
AnnotatedParameter param = candidate.parameter(i);
PropertyName name = candidate.paramName(i);
Boolean hasAnySetter = null;

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
Expand All @@ -879,10 +881,17 @@ protected void _addExplicitPropertyCreator(DeserializationContext ctxt,
*/
}
name = candidate.findImplicitParamName(i);
_validateNamedPropertyParameter(ctxt, beanDesc, candidate, i,
// [databind#562] Allow @JsonAnySetter in creators. Introspection is done here only because of
// _validateNamedPropertyParameter() check. Otherwise, in constructCreatorProperty seems appropriate....
hasAnySetter = ctxt.getAnnotationIntrospector().hasAnySetter(param);
if (Boolean.TRUE.equals(hasAnySetter)) {
JooHyukKim marked this conversation as resolved.
Show resolved Hide resolved
// no-op
} else {
_validateNamedPropertyParameter(ctxt, beanDesc, candidate, i,
name, injectId);
}
}
properties[i] = constructCreatorProperty(ctxt, beanDesc, name, i, param, injectId);
properties[i] = constructCreatorProperty(ctxt, beanDesc, name, i, param, injectId, hasAnySetter);
}
creators.addPropertyCreator(candidate.creator(), true, properties);
}
Expand Down Expand Up @@ -1187,7 +1196,8 @@ protected void _reportUnwrappedCreatorProperty(DeserializationContext ctxt,
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();
Expand Down Expand Up @@ -1225,7 +1235,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();
Expand All @@ -1238,6 +1248,19 @@ protected SettableBeanProperty constructCreatorProperty(DeserializationContext c
return prop;
}

/**
* @deprecated Since 2.18, use {@link #constructCreatorProperty(DeserializationContext, BeanDescription,
* PropertyName, int, AnnotatedParameter, JacksonInject.Value, Boolean)} instead.
*/
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, null);
}

private PropertyName _findParamName(AnnotatedParameter param, AnnotationIntrospector intr)
{
if (intr != null) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -497,7 +497,7 @@ protected Object _deserializeUsingPropertyBased(final JsonParser p, final Deseri
// "any property"?
if (_anySetter != null) {
try {
buffer.bufferAnyProperty(_anySetter, propName, _anySetter.deserialize(p, ctxt));
buffer.bufferMapProperty(propName, _anySetter.deserialize(p, ctxt));
} catch (Exception e) {
wrapAndThrow(e, _beanType.getRawClass(), propName, ctxt);
}
Expand All @@ -523,10 +523,15 @@ protected Object _deserializeUsingPropertyBased(final JsonParser p, final Deseri
// We hit END_OBJECT, so:
Object bean;
try {
bean = creator.build(ctxt, buffer);
if (_anySetter != null) {
bean = creator.buildWithAnySetter(ctxt, buffer, _anySetter);
} else {
bean = creator.build(ctxt, buffer);
}
} catch (Exception e) {
return wrapInstantiationProblem(e, ctxt);
}

// 13-Apr-2020, tatu: [databind#2678] need to handle injection here
if (_injectables != null) {
injectValues(ctxt, bean);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -544,8 +544,11 @@ protected void addBeanProps(DeserializationContext ctxt,

// Also, do we have a fallback "any" setter?
AnnotatedMember anySetter = beanDesc.findAnySetterAccessor();
AnnotatedMember creatorPropWithAnySetter = _findCreatorPropWithAnySetter(ctxt, 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...
Expand Down Expand Up @@ -661,6 +664,23 @@ protected void addBeanProps(DeserializationContext ctxt,
}
}

private AnnotatedMember _findCreatorPropWithAnySetter(DeserializationContext ctxt, SettableBeanProperty[] creatorProps) {
if (creatorProps != null) {
AnnotationIntrospector ai = ctxt.getAnnotationIntrospector();
JooHyukKim marked this conversation as resolved.
Show resolved Hide resolved
for (SettableBeanProperty prop : creatorProps) {
AnnotatedMember m = prop.getMember();
if (m != null) {
Boolean hasAnySetter = ai.hasAnySetter(m);
if (hasAnySetter != null && hasAnySetter) {
cowtowncoder marked this conversation as resolved.
Show resolved Hide resolved
return prop.getMember();
}
}

}
}
return null;
}

private boolean _isSetterlessType(Class<?> rawType) {
// May also need to consider getters
// for Map/Collection properties; but with lowest precedence
Expand Down Expand Up @@ -808,6 +828,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
Expand All @@ -819,6 +840,23 @@ protected SettableAnyProperty constructAnySetter(DeserializationContext ctxt,
valueType, null, mutator,
PropertyMetadata.STD_OPTIONAL);

} else if (mutator instanceof AnnotatedParameter){
AnnotatedParameter af = (AnnotatedParameter) mutator;
// get the type from the content type of the map object
JavaType fieldType = af.getType();
// 31-Jul-2022, tatu: Not just Maps any more but also JsonNode, so:
if (fieldType.isMapLikeType()) {
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 {
return ctxt.reportBadDefinition(beanDesc.getType(), String.format(
"Unsupported type for any-setter: %s -- only support `Map`s, `JsonNode` and `ObjectNode` ",
ClassUtil.getTypeDescription(fieldType)));
}
} else if (isField) {
AnnotatedField af = (AnnotatedField) mutator;
// get the type from the content type of the map object
Expand Down Expand Up @@ -880,6 +918,10 @@ protected SettableAnyProperty constructAnySetter(DeserializationContext ctxt,
return SettableAnyProperty.constructForMapField(ctxt,
prop, mutator, valueType, keyDeser, deser, typeDeser);
}
if (isMapParam) {
return SettableAnyProperty.constructForMapParameter(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 @@ -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;
Expand Down Expand Up @@ -76,19 +77,40 @@ 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 _isAnySetterProp;

/**
* @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 isAnySetterProp)
JooHyukKim marked this conversation as resolved.
Show resolved Hide resolved
{
super(name, type, wrapperName, typeDeser, contextAnnotations, metadata);
_annotated = param;
_creatorIndex = index;
_injectableValue = injectable;
_fallbackSetter = null;
_isAnySetterProp = isAnySetterProp; // [databind#562] Since 2.18
}

/**
* @since 2.11
* @deprecated Since 2.18. use factory later version instead.
*/
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, null);
}

/**
Expand Down Expand Up @@ -122,8 +144,24 @@ 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.
*/
cowtowncoder marked this conversation as resolved.
Show resolved Hide resolved
public static CreatorProperty construct(PropertyName name, JavaType type, PropertyName wrapperName,
TypeDeserializer typeDeser,
Expand All @@ -132,7 +170,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, null);
}

/**
Expand All @@ -145,6 +183,7 @@ protected CreatorProperty(CreatorProperty src, PropertyName newName) {
_fallbackSetter = src._fallbackSetter;
_creatorIndex = src._creatorIndex;
_ignorable = src._ignorable;
_isAnySetterProp = src._isAnySetterProp; // [databind#562] Since 2.18
}

protected CreatorProperty(CreatorProperty src, JsonDeserializer<?> deser,
Expand All @@ -155,6 +194,7 @@ protected CreatorProperty(CreatorProperty src, JsonDeserializer<?> deser,
_fallbackSetter = src._fallbackSetter;
_creatorIndex = src._creatorIndex;
_ignorable = src._ignorable;
_isAnySetterProp = src._isAnySetterProp; // [databind#562] Since 2.18
}

@Override
Expand Down Expand Up @@ -320,6 +360,13 @@ public boolean isInjectionOnly() {

// public boolean isInjectionOnly() { return false; }

/**
* @since 2.18
*/
public boolean isAnySetterProp() {
return Boolean.TRUE.equals(_isAnySetterProp);
}

/*
/**********************************************************
/* Overridden methods, other
Expand Down Expand Up @@ -354,4 +401,17 @@ private void _reportMissingSetter(JsonParser p, DeserializationContext ctxt) thr
throw InvalidDefinitionException.from(p, msg, getType());
}
}

/**
* @since 2.18
*/
public Map<Object, Object> initMap(DeserializationContext context, SettableAnyProperty anySetter)
JooHyukKim marked this conversation as resolved.
Show resolved Hide resolved
throws IOException
{
if (!isAnySetterProp()) {
throw new IllegalStateException("Cannot create Map for non-AnySetter creator property");
}
SettableAnyProperty.MapParameterAnyProperty mapParap = (SettableAnyProperty.MapParameterAnyProperty) anySetter;
JooHyukKim marked this conversation as resolved.
Show resolved Hide resolved
return mapParap.initMap(context);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -117,6 +117,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<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 MapParameterAnyProperty(property, field, valueType,
keyDeser, valueDeser, typeDeser,
vi);
}

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

Expand Down Expand Up @@ -437,4 +459,49 @@ public SettableAnyProperty withValueDeserializer(JsonDeserializer<Object> deser)
return this;
}
}

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

protected final ValueInstantiator _valueInstantiator;

public MapParameterAnyProperty(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 MapParameterAnyProperty(_property, _setter, _type,
_keyDeserializer, deser, _valueTypeDeserializer,
_valueInstantiator);
}

@SuppressWarnings("unchecked")
@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`");
}

protected Map<Object, Object> initMap(DeserializationContext ctxt)
throws IOException
{
if (_valueInstantiator == null) {
JooHyukKim marked this conversation as resolved.
Show resolved Hide resolved
throw JsonMappingException.from(ctxt, String.format(
"Cannot create an instance of %s for use as \"any-setter\" '%s'",
ClassUtil.nameOf(_type.getRawClass()), _property.getName()));
}
return (Map<Object, Object>) _valueInstantiator.createUsingDefault(ctxt);
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -301,6 +301,13 @@ public Object createFromObjectWith(DeserializationContext ctxt,
return createFromObjectWith(ctxt, buffer.getParameters(props));
}

public Object createFromObjectWith(DeserializationContext ctxt,
SettableBeanProperty[] props, PropertyValueBuffer buffer, SettableAnyProperty anySetter)
throws IOException
{
return createFromObjectWith(ctxt, buffer.getParametersWithAnySetter(props, anySetter));
}

/**
* Method to called to create value instance from JSON Object using
* an intermediate "delegate" value to pass to createor method
Expand Down