Skip to content

Commit 43ab288

Browse files
committed
Fix #3143
1 parent e7aaa58 commit 43ab288

File tree

9 files changed

+351
-58
lines changed

9 files changed

+351
-58
lines changed

release-notes/CREDITS-2.x

+3
Original file line numberDiff line numberDiff line change
@@ -1299,6 +1299,9 @@ Halil İbrahim Şener (hisener@github)
12991299
* Reported #2962: Auto-detection of constructor-based creator method skipped if there is
13001300
an annotated factory-based creator method (regression from 2.11)
13011301
(2.12.1)
1302+
* Reported #3143: String-based `Map` key deserializer is not deterministic when there is no
1303+
single arg constructor
1304+
(2.13.0)
13021305

13031306
Faron Dutton (fdutton@github)
13041307
* Contributed fix for #2990: Breaking API change in `BasicClassIntrospector` (2.12.0)

release-notes/VERSION-2.x

+3
Original file line numberDiff line numberDiff line change
@@ -42,6 +42,9 @@ Project: jackson-databind
4242
(reported by mistyzyq@github)
4343
#3130: Serializing java.lang.Thread fails on JDK 11 and above (should suppress
4444
serialization of ClassLoader)
45+
#3143: String-based `Map` key deserializer is not deterministic when there is no
46+
single arg constructor
47+
(reported by Halil İbrahim Ş)
4548
#3154: Add ArrayNode#set(int index, primitive_type value)
4649
(contributed by Tarekk Mohamed A)
4750
#3174: DOM `Node` serialization omits the default namespace declaration

src/main/java/com/fasterxml/jackson/databind/BeanDescription.java

+25
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@
44
import java.lang.reflect.Method;
55
import java.util.*;
66

7+
import com.fasterxml.jackson.annotation.JsonCreator;
78
import com.fasterxml.jackson.annotation.JsonFormat;
89
import com.fasterxml.jackson.annotation.JsonInclude;
910
import com.fasterxml.jackson.databind.annotation.JsonPOJOBuilder;
@@ -137,8 +138,24 @@ public boolean isNonStaticInnerClass() {
137138
/**********************************************************
138139
*/
139140

141+
/**
142+
* Helper method that will return all non-default constructors (that is,
143+
* constructors that take one or more arguments) this class has.
144+
*/
140145
public abstract List<AnnotatedConstructor> getConstructors();
141146

147+
/**
148+
* Method similar to {@link #getConstructors()} except will also introspect
149+
* {@code JsonCreator.Mode} and filter out ones marked as not applicable and
150+
* include mode (or lack thereof) for remaining constructors.
151+
*<p>
152+
* Note that no other filtering (regarding visibility or other annotations)
153+
* is performed
154+
*
155+
* @since 2.13
156+
*/
157+
public abstract List<AnnotatedAndMetadata<AnnotatedConstructor, JsonCreator.Mode>> getConstructorsWithMode();
158+
142159
/**
143160
* Helper method that will check all static methods of the bean class
144161
* that seem like factory methods eligible to be used as Creators.
@@ -159,6 +176,14 @@ public boolean isNonStaticInnerClass() {
159176
*/
160177
public abstract List<AnnotatedMethod> getFactoryMethods();
161178

179+
/**
180+
* Method similar to {@link #getFactoryMethods()} but will return {@code JsonCreator.Mode}
181+
* metadata along with qualifying factory method candidates.
182+
*
183+
* @since 2.13
184+
*/
185+
public abstract List<AnnotatedAndMetadata<AnnotatedMethod, JsonCreator.Mode>> getFactoryMethodsWithMode();
186+
162187
/**
163188
* Method that will locate the no-arg constructor for this class,
164189
* if it has one, and that constructor has not been marked as

src/main/java/com/fasterxml/jackson/databind/DeserializationContext.java

+11-3
Original file line numberDiff line numberDiff line change
@@ -663,15 +663,23 @@ public final JsonDeserializer<Object> findRootValueDeserializer(JavaType type)
663663
public final KeyDeserializer findKeyDeserializer(JavaType keyType,
664664
BeanProperty prop) throws JsonMappingException
665665
{
666-
KeyDeserializer kd = _cache.findKeyDeserializer(this,
667-
_factory, keyType);
666+
KeyDeserializer kd;
667+
// 15-Jun-2021, tatu: Needed wrt [databind#3143]
668+
try {
669+
kd = _cache.findKeyDeserializer(this, _factory, keyType);
670+
} catch (IllegalArgumentException iae) {
671+
// We better only expose checked exceptions, since those
672+
// are what caller is expected to handle
673+
reportBadDefinition(keyType, ClassUtil.exceptionMessage(iae));
674+
kd = null;
675+
}
668676
// Second: contextualize?
669677
if (kd instanceof ContextualKeyDeserializer) {
670678
kd = ((ContextualKeyDeserializer) kd).createContextual(this, prop);
671679
}
672680
return kd;
673681
}
674-
682+
675683
/*
676684
/**********************************************************
677685
/* Public API, ObjectId handling

src/main/java/com/fasterxml/jackson/databind/deser/std/StdKeyDeserializers.java

+107-15
Original file line numberDiff line numberDiff line change
@@ -2,9 +2,14 @@
22

33
import java.lang.reflect.Constructor;
44
import java.lang.reflect.Method;
5+
import java.util.List;
56

7+
import com.fasterxml.jackson.annotation.JsonCreator;
68
import com.fasterxml.jackson.databind.*;
79
import com.fasterxml.jackson.databind.deser.KeyDeserializers;
10+
import com.fasterxml.jackson.databind.introspect.AnnotatedAndMetadata;
11+
import com.fasterxml.jackson.databind.introspect.AnnotatedConstructor;
12+
import com.fasterxml.jackson.databind.introspect.AnnotatedMember;
813
import com.fasterxml.jackson.databind.introspect.AnnotatedMethod;
914
import com.fasterxml.jackson.databind.util.ClassUtil;
1015
import com.fasterxml.jackson.databind.util.EnumResolver;
@@ -47,36 +52,123 @@ public static KeyDeserializer constructDelegatingKeyDeserializer(Deserialization
4752

4853
public static KeyDeserializer findStringBasedKeyDeserializer(DeserializationConfig config,
4954
JavaType type)
50-
{
55+
throws JsonMappingException
56+
{
57+
// 15-Jun-2021, tatu: As per [databind#3143], full introspection needs to consider
58+
// as set of possibilities. Basically, precedence is:
59+
//
60+
// 1. Explicitly annotated 1-String-arg constructor, if one exists
61+
// 2. Explicitly annotated Factory method: just one allowed (exception if multiple)
62+
// 3. Implicit 1-String-arg constructor (no visibility checks for backwards
63+
// compatibility reasons; should probably be checked in future, 3.0?)
64+
// 4. Implicit Factory method with name of "valueOf()" (primary) or
65+
// "fromString()" (secondary). Likewise, no visibility check as of yet.
66+
5167
// We don't need full deserialization information, just need to know creators.
52-
BeanDescription beanDesc = config.introspect(type);
68+
final BeanDescription beanDesc = config.introspectForCreation(type);
5369
// Ok, so: can we find T(String) constructor?
54-
Constructor<?> ctor = beanDesc.findSingleArgConstructor(String.class);
55-
if (ctor != null) {
56-
if (config.canOverrideAccessModifiers()) {
57-
ClassUtil.checkAndFixAccess(ctor, config.isEnabled(MapperFeature.OVERRIDE_PUBLIC_ACCESS_MODIFIERS));
58-
}
59-
return new StdKeyDeserializer.StringCtorKeyDeserializer(ctor);
70+
final AnnotatedAndMetadata<AnnotatedConstructor, JsonCreator.Mode> ctorInfo = _findStringConstructor(beanDesc);
71+
// Explicit?
72+
if ((ctorInfo != null) && (ctorInfo.metadata != null)) {
73+
return _constructCreatorKeyDeserializer(config, ctorInfo.annotated);
6074
}
6175
// or if not, "static T valueOf(String)" (or equivalent marked
6276
// with @JsonCreator annotation?)
63-
Method m = beanDesc.findFactoryMethod(String.class);
64-
if (m != null){
77+
final List<AnnotatedAndMetadata<AnnotatedMethod, JsonCreator.Mode>> factoryCandidates
78+
= beanDesc.getFactoryMethodsWithMode();
79+
80+
// But must now filter out invalid candidates, both by signature (must take 1 and
81+
// only 1 arg; that arg must be of type `String`) and by annotations (we only
82+
// accept "delegating" style, so remove PROPERTIES)
83+
factoryCandidates.removeIf(m ->
84+
(m.annotated.getParameterCount() != 1)
85+
|| (m.annotated.getRawParameterType(0) != String.class)
86+
|| (m.metadata == JsonCreator.Mode.PROPERTIES)
87+
);
88+
89+
// Any explicit?
90+
final AnnotatedMethod explicitFactory = _findExplicitStringFactoryMethod(factoryCandidates);
91+
if (explicitFactory != null) {
92+
return _constructCreatorKeyDeserializer(config, explicitFactory);
93+
}
94+
// If we had implicit Constructor, that'd work now
95+
if (ctorInfo != null) {
96+
return _constructCreatorKeyDeserializer(config, ctorInfo.annotated);
97+
}
98+
// And finally, if any implicit factory methods, acceptable now
99+
// nope, no such luck...
100+
if (!factoryCandidates.isEmpty()) {
101+
// 15-Jun-2021, tatu: Ideally we would provide stabler ordering, but for now
102+
// let's simply pick the first one
103+
return _constructCreatorKeyDeserializer(config, factoryCandidates.get(0).annotated);
104+
}
105+
return null;
106+
}
107+
108+
private static KeyDeserializer _constructCreatorKeyDeserializer(DeserializationConfig config,
109+
AnnotatedMember creator)
110+
{
111+
if (creator instanceof AnnotatedConstructor) {
112+
Constructor<?> rawCtor = ((AnnotatedConstructor) creator).getAnnotated();
65113
if (config.canOverrideAccessModifiers()) {
66-
ClassUtil.checkAndFixAccess(m, config.isEnabled(MapperFeature.OVERRIDE_PUBLIC_ACCESS_MODIFIERS));
114+
ClassUtil.checkAndFixAccess(rawCtor, config.isEnabled(MapperFeature.OVERRIDE_PUBLIC_ACCESS_MODIFIERS));
115+
}
116+
return new StdKeyDeserializer.StringCtorKeyDeserializer(rawCtor);
117+
}
118+
Method m = ((AnnotatedMethod) creator).getAnnotated();
119+
if (config.canOverrideAccessModifiers()) {
120+
ClassUtil.checkAndFixAccess(m, config.isEnabled(MapperFeature.OVERRIDE_PUBLIC_ACCESS_MODIFIERS));
121+
}
122+
return new StdKeyDeserializer.StringFactoryKeyDeserializer(m);
123+
}
124+
125+
// 13-Jun-2021, tatu: For now just look for constructor that takes one `String`
126+
// argument (could look for CharSequence) and hence can have just one, no dups
127+
private static AnnotatedAndMetadata<AnnotatedConstructor, JsonCreator.Mode> _findStringConstructor(BeanDescription beanDesc)
128+
{
129+
for (AnnotatedAndMetadata<AnnotatedConstructor, JsonCreator.Mode> entry
130+
: beanDesc.getConstructorsWithMode())
131+
{
132+
// BeanDescription method does NOT filter out various types so check
133+
// it takes single argument.
134+
final AnnotatedConstructor ctor = entry.annotated;
135+
if ((ctor.getParameterCount() == 1)
136+
&& (String.class == ctor.getRawParameterType(0))) {
137+
return entry;
67138
}
68-
return new StdKeyDeserializer.StringFactoryKeyDeserializer(m);
69139
}
70-
// nope, no such luck...
71140
return null;
72141
}
73-
142+
143+
private static AnnotatedMethod _findExplicitStringFactoryMethod(
144+
List<AnnotatedAndMetadata<AnnotatedMethod, JsonCreator.Mode>> candidates)
145+
throws JsonMappingException
146+
{
147+
AnnotatedMethod match = null;
148+
for (AnnotatedAndMetadata<AnnotatedMethod, JsonCreator.Mode> entry : candidates) {
149+
// Note: caller has filtered out invalid candidates; all we need to check are dups
150+
if (entry.metadata != null) {
151+
if (match == null) {
152+
match = entry.annotated;
153+
} else {
154+
// 15-Jun-2021, tatu: Not optimal type or information, but has to do for now
155+
// since we do not get DeserializationContext
156+
Class<?> rawKeyType = entry.annotated.getDeclaringClass();
157+
throw new IllegalArgumentException(
158+
"Multiple suitable annotated Creator factory methods to be used as the Key deserializer for type "
159+
+ClassUtil.nameOf(rawKeyType));
160+
}
161+
}
162+
}
163+
return match;
164+
}
165+
74166
/*
75167
/**********************************************************
76168
/* KeyDeserializers implementation
77169
/**********************************************************
78170
*/
79-
171+
80172
@Override
81173
public KeyDeserializer findKeyDeserializer(JavaType type,
82174
DeserializationConfig config, BeanDescription beanDesc) throws JsonMappingException
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,22 @@
1+
package com.fasterxml.jackson.databind.introspect;
2+
3+
/**
4+
* Silly little "Pair" class needed for 2-element tuples (without
5+
* adding dependency to one of 3rd party packages that has one).
6+
*
7+
* @since 2.13
8+
*/
9+
public class AnnotatedAndMetadata<A extends Annotated, M extends Object>
10+
{
11+
public final A annotated;
12+
public final M metadata;
13+
14+
public AnnotatedAndMetadata(A ann, M md) {
15+
annotated = ann;
16+
metadata = md;
17+
}
18+
19+
public static <A extends Annotated, M> AnnotatedAndMetadata<A, M> of(A ann, M md) {
20+
return new AnnotatedAndMetadata<>(ann, md);
21+
}
22+
}

src/main/java/com/fasterxml/jackson/databind/introspect/BasicBeanDescription.java

+80
Original file line numberDiff line numberDiff line change
@@ -346,6 +346,23 @@ public List<AnnotatedConstructor> getConstructors() {
346346
return _classInfo.getConstructors();
347347
}
348348

349+
@Override
350+
public List<AnnotatedAndMetadata<AnnotatedConstructor, JsonCreator.Mode>> getConstructorsWithMode() {
351+
List<AnnotatedConstructor> allCtors = _classInfo.getConstructors();
352+
if (allCtors.isEmpty()) {
353+
return Collections.emptyList();
354+
}
355+
List<AnnotatedAndMetadata<AnnotatedConstructor, JsonCreator.Mode>> result = new ArrayList<>();
356+
for (AnnotatedConstructor ctor : allCtors) {
357+
JsonCreator.Mode mode = _annotationIntrospector.findCreatorAnnotation(_config, ctor);
358+
if (mode == JsonCreator.Mode.DISABLED) {
359+
continue;
360+
}
361+
result.add(AnnotatedAndMetadata.of(ctor, mode));
362+
}
363+
return result;
364+
}
365+
349366
@Override
350367
public Object instantiateBean(boolean fixAccess) {
351368
AnnotatedConstructor ac = _classInfo.getDefaultConstructor();
@@ -571,6 +588,30 @@ public List<AnnotatedMethod> getFactoryMethods()
571588
return result;
572589
}
573590

591+
@Override // since 2.13
592+
public List<AnnotatedAndMetadata<AnnotatedMethod, JsonCreator.Mode>> getFactoryMethodsWithMode()
593+
{
594+
List<AnnotatedMethod> candidates = _classInfo.getFactoryMethods();
595+
if (candidates.isEmpty()) {
596+
return Collections.emptyList();
597+
}
598+
List<AnnotatedAndMetadata<AnnotatedMethod, JsonCreator.Mode>> result = null;
599+
for (AnnotatedMethod am : candidates) {
600+
AnnotatedAndMetadata<AnnotatedMethod, JsonCreator.Mode> match
601+
= findFactoryMethodMetadata(am);
602+
if (match != null) {
603+
if (result == null) {
604+
result = new ArrayList<>();
605+
}
606+
result.add(match);
607+
}
608+
}
609+
if (result == null) {
610+
return Collections.emptyList();
611+
}
612+
return result;
613+
}
614+
574615
@Override
575616
@Deprecated // since 2.13
576617
public Constructor<?> findSingleArgConstructor(Class<?>... argTypes)
@@ -646,6 +687,45 @@ protected boolean isFactoryMethod(AnnotatedMethod am)
646687
return false;
647688
}
648689

690+
// @since 2.13
691+
protected AnnotatedAndMetadata<AnnotatedMethod, JsonCreator.Mode> findFactoryMethodMetadata(AnnotatedMethod am)
692+
{
693+
// First: return type must be compatible with the introspected class
694+
// (i.e. allowed to be sub-class, although usually is the same class)
695+
Class<?> rt = am.getRawReturnType();
696+
if (!getBeanClass().isAssignableFrom(rt)) {
697+
return null;
698+
}
699+
// Also: must be a recognized factory method, meaning:
700+
// (a) marked with @JsonCreator annotation, or
701+
// (b) 1-argument "valueOf" (at this point, need not be public), or
702+
// (c) 1-argument "fromString()" AND takes {@code String} as the argument
703+
JsonCreator.Mode mode = _annotationIntrospector.findCreatorAnnotation(_config, am);
704+
if (mode != null) {
705+
if (mode == JsonCreator.Mode.DISABLED) {
706+
return null;
707+
}
708+
return AnnotatedAndMetadata.of(am, mode);
709+
}
710+
final String name = am.getName();
711+
// 24-Oct-2016, tatu: As per [databind#1429] must ensure takes exactly one arg
712+
if ("valueOf".equals(name)) {
713+
if (am.getParameterCount() == 1) {
714+
return AnnotatedAndMetadata.of(am, mode);
715+
}
716+
}
717+
// [databind#208] Also accept "fromString()", if takes String or CharSequence
718+
if ("fromString".equals(name)) {
719+
if (am.getParameterCount() == 1) {
720+
Class<?> cls = am.getRawParameterType(0);
721+
if (cls == String.class || CharSequence.class.isAssignableFrom(cls)) {
722+
return AnnotatedAndMetadata.of(am, mode);
723+
}
724+
}
725+
}
726+
return null;
727+
}
728+
649729
/**
650730
* @deprecated since 2.8
651731
*/

0 commit comments

Comments
 (0)