Skip to content

Commit 9147dba

Browse files
authored
Finish BasicDeserializerFactory refactoring (complete #4515) (#4543)
1 parent 590f754 commit 9147dba

File tree

12 files changed

+306
-722
lines changed

12 files changed

+306
-722
lines changed

release-notes/VERSION-2.x

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,7 @@ Project: jackson-databind
2424
(contributed by @pjfanning)
2525
#4483: Remove `final` on method BeanSerializer.serialize()
2626
(contributed by Matthew L)
27+
#4515: Rewrite Bean Property Introspection logic in Jackson 2.x
2728

2829
2.17.1 (04-May-2024)
2930

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

Lines changed: 78 additions & 664 deletions
Large diffs are not rendered by default.

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

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -259,6 +259,11 @@ protected boolean _isStdJDKCollection(JavaType type)
259259
// of matches), or ambitious? Let's do latter for now.
260260
if (Collection.class.isAssignableFrom(raw)
261261
|| Map.class.isAssignableFrom(raw)) {
262+
// 28-May-2024, tatu: Complications wrt [databind#4515] / [databind#2795]
263+
// mean that partial introspection NOT for inner classes
264+
if (raw.toString().indexOf('$') > 0) {
265+
return false;
266+
}
262267
return true;
263268
}
264269
}

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

Lines changed: 168 additions & 34 deletions
Original file line numberDiff line numberDiff line change
@@ -662,6 +662,8 @@ protected void _addCreators(Map<String, POJOPropertyBuilder> props)
662662
// Next: remove creators marked as explicitly disabled
663663
_removeDisabledCreators(constructors);
664664
_removeDisabledCreators(factories);
665+
// And then remove non-annotated static methods that do not look like factories
666+
_removeNonFactoryStaticMethods(factories);
665667

666668
// and use annotations to find explicitly chosen Creators
667669
if (_useAnnotations) { // can't have explicit ones without Annotation introspection
@@ -683,9 +685,30 @@ protected void _addCreators(Map<String, POJOPropertyBuilder> props)
683685
// (JDK 17 Record/Scala/Kotlin)
684686
if (!creators.hasPropertiesBased()) {
685687
// for Records:
686-
if ((canonical != null) && constructors.contains(canonical)) {
687-
constructors.remove(canonical);
688-
creators.setPropertiesBased(_config, canonical, "canonical");
688+
if (canonical != null) {
689+
// ... but only process if still included as a candidate
690+
if (constructors.remove(canonical)) {
691+
// But wait! Could be delegating
692+
if (_isDelegatingConstructor(canonical)) {
693+
creators.addExplicitDelegating(canonical);
694+
} else {
695+
creators.setPropertiesBased(_config, canonical, "canonical");
696+
}
697+
}
698+
}
699+
}
700+
701+
// One more thing: if neither explicit (constructor or factory) nor
702+
// canonical (constructor?), consider implicit Constructor with
703+
// all named.
704+
final ConstructorDetector ctorDetector = _config.getConstructorDetector();
705+
if (!creators.hasPropertiesBasedOrDelegating()
706+
&& !ctorDetector.requireCtorAnnotation()) {
707+
// But only if no default constructor available OR if we are configured
708+
// to prefer properties-based Creators
709+
if ((_classDef.getDefaultConstructor() == null)
710+
|| ctorDetector.singleArgCreatorDefaultsToProperties()) {
711+
_addImplicitConstructor(creators, constructors, props);
689712
}
690713
}
691714

@@ -703,6 +726,20 @@ protected void _addCreators(Map<String, POJOPropertyBuilder> props)
703726
}
704727
}
705728

729+
// Method to determine if given non-explictly-annotated constructor
730+
// looks like delegating one
731+
private boolean _isDelegatingConstructor(PotentialCreator ctor)
732+
{
733+
// Only consider single-arg case, for now
734+
if (ctor.paramCount() == 1) {
735+
// Main thing: @JsonValue makes it delegating:
736+
if ((_jsonValueAccessors != null) && !_jsonValueAccessors.isEmpty()) {
737+
return true;
738+
}
739+
}
740+
return false;
741+
}
742+
706743
private List<PotentialCreator> _collectCreators(List<? extends AnnotatedWithParams> ctors)
707744
{
708745
if (ctors.isEmpty()) {
@@ -728,6 +765,35 @@ private void _removeDisabledCreators(List<PotentialCreator> ctors)
728765
}
729766
}
730767

768+
private void _removeNonFactoryStaticMethods(List<PotentialCreator> ctors)
769+
{
770+
final Class<?> rawType = _type.getRawClass();
771+
Iterator<PotentialCreator> it = ctors.iterator();
772+
while (it.hasNext()) {
773+
// explicit mode? Retain (for now)
774+
PotentialCreator ctor = it.next();
775+
if (ctor.creatorMode() != null) {
776+
continue;
777+
}
778+
// Copied from `BasicBeanDescription.isFactoryMethod()`
779+
AnnotatedWithParams factory = ctor.creator();
780+
if (rawType.isAssignableFrom(factory.getRawType())
781+
&& ctor.paramCount() == 1) {
782+
String name = factory.getName();
783+
784+
if ("valueOf".equals(name)) {
785+
continue;
786+
} else if ("fromString".equals(name)) {
787+
Class<?> cls = factory.getRawParameterType(0);
788+
if (cls == String.class || CharSequence.class.isAssignableFrom(cls)) {
789+
continue;
790+
}
791+
}
792+
}
793+
it.remove();
794+
}
795+
}
796+
731797
private void _addExplicitlyAnnotatedCreators(PotentialCreators collector,
732798
List<PotentialCreator> ctors,
733799
Map<String, POJOPropertyBuilder> props,
@@ -743,48 +809,25 @@ private void _addExplicitlyAnnotatedCreators(PotentialCreators collector,
743809
if (ctor.creatorMode() == null) {
744810
continue;
745811
}
812+
746813
it.remove();
747814

748-
Boolean propsBased = null;
749-
815+
boolean isPropsBased;
816+
750817
switch (ctor.creatorMode()) {
751818
case DELEGATING:
752-
propsBased = false;
819+
isPropsBased = false;
753820
break;
754821
case PROPERTIES:
755-
propsBased = true;
822+
isPropsBased = true;
756823
break;
757824
case DEFAULT:
758825
default:
759-
// First things first: if not single-arg Creator, must be Properties-based
760-
// !!! Or does it? What if there's @JacksonInject etc?
761-
if (ctor.paramCount() != 1) {
762-
propsBased = true;
763-
}
764-
}
765-
766-
// Must be 1-arg case:
767-
if (propsBased == null) {
768-
// Is ambiguity/heuristics allowed?
769-
if (ctorDetector.requireCtorAnnotation()) {
770-
throw new IllegalArgumentException(String.format(
771-
"Ambiguous 1-argument Creator; `ConstructorDetector` requires specifying `mode`: %s",
772-
ctor));
773-
}
774-
775-
// First things first: if explicit names found, is Properties-based
776-
ctor.introspectParamNames(_config);
777-
propsBased = ctor.hasExplicitNames()
778-
|| ctorDetector.singleArgCreatorDefaultsToProperties();
779-
// One more possibility: implicit name that maps to implied
780-
// property based on Field/Getter/Setter
781-
if (!propsBased) {
782-
String implName = ctor.implicitNameSimple(0);
783-
propsBased = (implName != null) && props.containsKey(implName);
784-
}
826+
isPropsBased = _isExplicitlyAnnotatedCreatorPropsBased(ctor,
827+
props, ctorDetector);
785828
}
786829

787-
if (propsBased) {
830+
if (isPropsBased) {
788831
// Skipping done if we already got higher-precedence Creator
789832
if (!skipPropsBased) {
790833
collector.setPropertiesBased(_config, ctor, "explicit");
@@ -795,6 +838,53 @@ private void _addExplicitlyAnnotatedCreators(PotentialCreators collector,
795838
}
796839
}
797840

841+
private boolean _isExplicitlyAnnotatedCreatorPropsBased(PotentialCreator ctor,
842+
Map<String, POJOPropertyBuilder> props, ConstructorDetector ctorDetector)
843+
{
844+
if (ctor.paramCount() == 1) {
845+
// Is ambiguity/heuristics allowed?
846+
switch (ctorDetector.singleArgMode()) {
847+
case DELEGATING:
848+
return false;
849+
case PROPERTIES:
850+
return true;
851+
case REQUIRE_MODE:
852+
throw new IllegalArgumentException(String.format(
853+
"Single-argument constructor (%s) is annotated but no 'mode' defined; `ConstructorDetector`"
854+
+ "configured with `SingleArgConstructor.REQUIRE_MODE`",
855+
ctor.creator()));
856+
case HEURISTIC:
857+
default:
858+
}
859+
}
860+
861+
// First: if explicit names found, is Properties-based
862+
ctor.introspectParamNames(_config);
863+
if (ctor.hasExplicitNames()) {
864+
return true;
865+
}
866+
// Second: [databind#3180] @JsonValue indicates delegating
867+
if ((_jsonValueAccessors != null) && !_jsonValueAccessors.isEmpty()) {
868+
return false;
869+
}
870+
if (ctor.paramCount() == 1) {
871+
// One more possibility: implicit name that maps to implied
872+
// property based on Field/Getter/Setter
873+
String implName = ctor.implicitNameSimple(0);
874+
if ((implName != null) && props.containsKey(implName)) {
875+
return true;
876+
}
877+
// Second: injectable also suffices
878+
if ((_annotationIntrospector != null)
879+
&& _annotationIntrospector.findInjectableValue(ctor.param(0)) != null) {
880+
return true;
881+
}
882+
return false;
883+
}
884+
// Trickiest case: rely on existence of implicit names and/or injectables
885+
return ctor.hasNameOrInjectForAllParams(_config);
886+
}
887+
798888
private void _addCreatorsWithAnnotatedNames(PotentialCreators collector,
799889
List<PotentialCreator> ctors)
800890
{
@@ -813,6 +903,50 @@ private void _addCreatorsWithAnnotatedNames(PotentialCreators collector,
813903
}
814904
}
815905

906+
private boolean _addImplicitConstructor(PotentialCreators collector,
907+
List<PotentialCreator> ctors, Map<String, POJOPropertyBuilder> props)
908+
{
909+
// Must have one and only one candidate
910+
if (ctors.size() != 1) {
911+
return false;
912+
}
913+
final PotentialCreator ctor = ctors.get(0);
914+
// which needs to be visible
915+
if (!_visibilityChecker.isCreatorVisible(ctor.creator())) {
916+
return false;
917+
}
918+
ctor.introspectParamNames(_config);
919+
920+
// As usual, 1-param case is distinct
921+
if (ctor.paramCount() != 1) {
922+
if (!ctor.hasNameOrInjectForAllParams(_config)) {
923+
return false;
924+
}
925+
} else {
926+
// First things first: if only param has Injectable, must be Props-based
927+
if ((_annotationIntrospector != null)
928+
&& _annotationIntrospector.findInjectableValue(ctor.param(0)) != null) {
929+
// props-based, continue
930+
} else {
931+
// may have explicit preference
932+
final ConstructorDetector ctorDetector = _config.getConstructorDetector();
933+
if (ctorDetector.singleArgCreatorDefaultsToDelegating()) {
934+
return false;
935+
}
936+
// if not, prefer Properties-based if explicit preference OR
937+
// property with same name
938+
if (!ctorDetector.singleArgCreatorDefaultsToProperties()
939+
&& !props.containsKey(ctor.implicitNameSimple(0))) {
940+
return false;
941+
}
942+
}
943+
}
944+
945+
ctors.remove(0);
946+
collector.setPropertiesBased(_config, ctor, "implicit");
947+
return true;
948+
}
949+
816950
private void _addCreatorParams(Map<String, POJOPropertyBuilder> props,
817951
PotentialCreator ctor, List<POJOPropertyBuilder> creatorProps)
818952
{

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

Lines changed: 19 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -142,10 +142,28 @@ public boolean hasExplicitNames() {
142142
return false;
143143
}
144144

145+
public boolean hasNameFor(int ix) {
146+
return (explicitParamNames[ix] != null)
147+
|| (implicitParamNames[ix] != null);
148+
}
149+
150+
public boolean hasNameOrInjectForAllParams(MapperConfig<?> config)
151+
{
152+
final AnnotationIntrospector intr = config.getAnnotationIntrospector();
153+
for (int i = 0, end = implicitParamNames.length; i < end; ++i) {
154+
if (!hasNameFor(i)) {
155+
if (intr == null || intr.findInjectableValue(creator.getParameter(i)) == null) {
156+
return false;
157+
}
158+
}
159+
}
160+
return true;
161+
}
162+
145163
public PropertyName explicitName(int ix) {
146164
return explicitParamNames[ix];
147165
}
148-
166+
149167
public PropertyName implicitName(int ix) {
150168
return implicitParamNames[ix];
151169
}

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

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -69,10 +69,10 @@ public List<PotentialCreator> getExplicitDelegating() {
6969
}
7070

7171
public List<PotentialCreator> getImplicitDelegatingFactories() {
72-
return implicitDelegatingFactories;
72+
return (implicitDelegatingFactories == null) ? Collections.emptyList() : implicitDelegatingFactories;
7373
}
7474

7575
public List<PotentialCreator> getImplicitDelegatingConstructors() {
76-
return implicitDelegatingConstructors;
76+
return (implicitDelegatingConstructors == null) ? Collections.emptyList() : implicitDelegatingConstructors;
7777
}
7878
}

src/test-jdk17/java/com/fasterxml/jackson/databind/records/RecordCreatorsTest.java

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -34,7 +34,7 @@ public RecordWithDelegation(String value) {
3434
this.value = "del:"+value;
3535
}
3636

37-
@JsonValue()
37+
@JsonValue
3838
public String getValue() {
3939
return "val:"+value;
4040
}

src/test-jdk17/java/com/fasterxml/jackson/databind/records/RecordExplicitCreatorsTest.java

Lines changed: 7 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -197,18 +197,15 @@ public void testDeserializeUsingCanonicalConstructor_WhenJsonCreatorConstructorE
197197
}
198198
}
199199

200+
// 23-May-2024, tatu: Logic changed as part of [databind#4515]: explicit properties-based
201+
// Creator does NOT block implicit delegating Creators. So formerly (pre-2.18) failing
202+
// case is now expected to pass.
200203
@Test
201204
public void testDeserializeUsingImplicitFactoryMethod_WhenJsonCreatorConstructorExists_WillFail() throws Exception {
202-
try {
203-
MAPPER.readValue("123", RecordWithJsonPropertyWithJsonCreator.class);
204-
205-
fail("should not pass");
206-
} catch (MismatchedInputException e) {
207-
verifyException(e, "Cannot construct instance");
208-
verifyException(e, "RecordWithJsonPropertyWithJsonCreator");
209-
verifyException(e, "although at least one Creator exists");
210-
verifyException(e, "no int/Int-argument constructor/factory method");
211-
}
205+
RecordWithJsonPropertyWithJsonCreator value = MAPPER.readValue("123",
206+
RecordWithJsonPropertyWithJsonCreator.class);
207+
assertEquals(123, value.id());
208+
assertEquals("JsonCreatorConstructor", value.name());
212209
}
213210

214211
/*

src/test/java/com/fasterxml/jackson/databind/deser/creators/ConstructorDetector1498Test.java

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@
99
import com.fasterxml.jackson.databind.*;
1010
import com.fasterxml.jackson.databind.cfg.*;
1111
import com.fasterxml.jackson.databind.exc.InvalidDefinitionException;
12+
import com.fasterxml.jackson.databind.exc.UnrecognizedPropertyException;
1213
import com.fasterxml.jackson.databind.introspect.AnnotatedMember;
1314
import com.fasterxml.jackson.databind.introspect.JacksonAnnotationIntrospector;
1415
import com.fasterxml.jackson.databind.json.JsonMapper;
@@ -147,13 +148,20 @@ public void testMultiArgAsProperties() throws Exception
147148
@Test
148149
public void test1ArgDefaultsToPropsMultipleCtors() throws Exception
149150
{
151+
// 23-May-2024, tatu: Will fail differently with [databind#4515]; default
152+
// constructor available, implicit ones ignored
150153
try {
151154
MAPPER_PROPS.readValue(a2q("{'value' : 137 }"),
152155
SingleArg2CtorsNotAnnotated.class);
153156
fail("Should not pass");
157+
} catch (UnrecognizedPropertyException e) {
158+
verifyException(e, "\"value\"");
159+
}
160+
/*
154161
} catch (InvalidDefinitionException e) {
155162
verifyException(e, "Conflicting property-based creators");
156163
}
164+
*/
157165
}
158166

159167
/*

0 commit comments

Comments
 (0)