Skip to content

Commit 246fd12

Browse files
authored
Merge #4515 from 2.18 to master (#4547)
1 parent 93128d6 commit 246fd12

File tree

10 files changed

+304
-722
lines changed

10 files changed

+304
-722
lines changed

src/main/java/tools/jackson/databind/deser/BasicDeserializerFactory.java

+80-664
Large diffs are not rendered by default.

src/main/java/tools/jackson/databind/introspect/POJOPropertiesCollector.java

+193-34
Original file line numberDiff line numberDiff line change
@@ -609,6 +609,8 @@ protected void _addCreators(Map<String, POJOPropertyBuilder> props)
609609
// Next: remove creators marked as explicitly disabled
610610
_removeDisabledCreators(constructors);
611611
_removeDisabledCreators(factories);
612+
// And then remove non-annotated static methods that do not look like factories
613+
_removeNonFactoryStaticMethods(factories);
612614

613615
// and use annotations to find explicitly chosen Creators
614616
if (_useAnnotations) { // can't have explicit ones without Annotation introspection
@@ -630,13 +632,37 @@ protected void _addCreators(Map<String, POJOPropertyBuilder> props)
630632
// (JDK 17 Record/Scala/Kotlin)
631633
if (!creators.hasPropertiesBased()) {
632634
// for Records:
633-
if ((canonical != null) && constructors.contains(canonical)) {
634-
constructors.remove(canonical);
635-
creators.setPropertiesBased(_config, canonical, "canonical");
635+
if (canonical != null) {
636+
// ... but only process if still included as a candidate
637+
if (constructors.remove(canonical)) {
638+
// But wait! Could be delegating
639+
if (_isDelegatingConstructor(canonical)) {
640+
creators.addExplicitDelegating(canonical);
641+
} else {
642+
creators.setPropertiesBased(_config, canonical, "canonical");
643+
}
644+
}
645+
}
646+
}
647+
648+
// One more thing: if neither explicit (constructor or factory) nor
649+
// canonical (constructor?), consider implicit Constructor with
650+
// all named.
651+
final ConstructorDetector ctorDetector = _config.getConstructorDetector();
652+
if (!creators.hasPropertiesBasedOrDelegating()
653+
&& !ctorDetector.requireCtorAnnotation()) {
654+
// But only if no default constructor available OR if we are configured
655+
// to prefer properties-based Creators
656+
if ((_classDef.getDefaultConstructor() == null)
657+
|| ctorDetector.singleArgCreatorDefaultsToProperties()) {
658+
_addImplicitConstructor(creators, constructors, props);
636659
}
637660
}
638661

639662
// Anything else left, add as possible implicit Creators
663+
// ... but first, trim non-visible
664+
_removeNonVisibleCreators(constructors);
665+
_removeNonVisibleCreators(factories);
640666
creators.setImplicitDelegating(constructors, factories);
641667

642668
// And finally add logical properties for the One Properties-based
@@ -650,6 +676,20 @@ protected void _addCreators(Map<String, POJOPropertyBuilder> props)
650676
}
651677
}
652678

679+
// Method to determine if given non-explictly-annotated constructor
680+
// looks like delegating one
681+
private boolean _isDelegatingConstructor(PotentialCreator ctor)
682+
{
683+
// Only consider single-arg case, for now
684+
if (ctor.paramCount() == 1) {
685+
// Main thing: @JsonValue makes it delegating:
686+
if ((_jsonValueAccessors != null) && !_jsonValueAccessors.isEmpty()) {
687+
return true;
688+
}
689+
}
690+
return false;
691+
}
692+
653693
private List<PotentialCreator> _collectCreators(List<? extends AnnotatedWithParams> ctors)
654694
{
655695
if (ctors.isEmpty()) {
@@ -675,6 +715,49 @@ private void _removeDisabledCreators(List<PotentialCreator> ctors)
675715
}
676716
}
677717

718+
private void _removeNonVisibleCreators(List<PotentialCreator> ctors)
719+
{
720+
Iterator<PotentialCreator> it = ctors.iterator();
721+
while (it.hasNext()) {
722+
PotentialCreator ctor = it.next();
723+
boolean visible = (ctor.paramCount() == 1)
724+
? _visibilityChecker.isScalarConstructorVisible(ctor.creator())
725+
: _visibilityChecker.isCreatorVisible(ctor.creator());
726+
if (!visible) {
727+
it.remove();
728+
}
729+
}
730+
}
731+
732+
private void _removeNonFactoryStaticMethods(List<PotentialCreator> ctors)
733+
{
734+
final Class<?> rawType = _type.getRawClass();
735+
Iterator<PotentialCreator> it = ctors.iterator();
736+
while (it.hasNext()) {
737+
// explicit mode? Retain (for now)
738+
PotentialCreator ctor = it.next();
739+
if (ctor.creatorMode() != null) {
740+
continue;
741+
}
742+
// Copied from `BasicBeanDescription.isFactoryMethod()`
743+
AnnotatedWithParams factory = ctor.creator();
744+
if (rawType.isAssignableFrom(factory.getRawType())
745+
&& ctor.paramCount() == 1) {
746+
String name = factory.getName();
747+
748+
if ("valueOf".equals(name)) {
749+
continue;
750+
} else if ("fromString".equals(name)) {
751+
Class<?> cls = factory.getRawParameterType(0);
752+
if (cls == String.class || CharSequence.class.isAssignableFrom(cls)) {
753+
continue;
754+
}
755+
}
756+
}
757+
it.remove();
758+
}
759+
}
760+
678761
private void _addExplicitlyAnnotatedCreators(PotentialCreators collector,
679762
List<PotentialCreator> ctors,
680763
Map<String, POJOPropertyBuilder> props,
@@ -690,48 +773,25 @@ private void _addExplicitlyAnnotatedCreators(PotentialCreators collector,
690773
if (ctor.creatorMode() == null) {
691774
continue;
692775
}
776+
693777
it.remove();
694778

695-
Boolean propsBased = null;
696-
779+
boolean isPropsBased;
780+
697781
switch (ctor.creatorMode()) {
698782
case DELEGATING:
699-
propsBased = false;
783+
isPropsBased = false;
700784
break;
701785
case PROPERTIES:
702-
propsBased = true;
786+
isPropsBased = true;
703787
break;
704788
case DEFAULT:
705789
default:
706-
// First things first: if not single-arg Creator, must be Properties-based
707-
// !!! Or does it? What if there's @JacksonInject etc?
708-
if (ctor.paramCount() != 1) {
709-
propsBased = true;
710-
}
790+
isPropsBased = _isExplicitlyAnnotatedCreatorPropsBased(ctor,
791+
props, ctorDetector);
711792
}
712793

713-
// Must be 1-arg case:
714-
if (propsBased == null) {
715-
// Is ambiguity/heuristics allowed?
716-
if (ctorDetector.requireCtorAnnotation()) {
717-
throw new IllegalArgumentException(String.format(
718-
"Ambiguous 1-argument Creator; `ConstructorDetector` requires specifying `mode`: %s",
719-
ctor));
720-
}
721-
722-
// First things first: if explicit names found, is Properties-based
723-
ctor.introspectParamNames(_config);
724-
propsBased = ctor.hasExplicitNames()
725-
|| ctorDetector.singleArgCreatorDefaultsToProperties();
726-
// One more possibility: implicit name that maps to implied
727-
// property based on Field/Getter/Setter
728-
if (!propsBased) {
729-
String implName = ctor.implicitNameSimple(0);
730-
propsBased = (implName != null) && props.containsKey(implName);
731-
}
732-
}
733-
734-
if (propsBased) {
794+
if (isPropsBased) {
735795
// Skipping done if we already got higher-precedence Creator
736796
if (!skipPropsBased) {
737797
collector.setPropertiesBased(_config, ctor, "explicit");
@@ -742,6 +802,56 @@ private void _addExplicitlyAnnotatedCreators(PotentialCreators collector,
742802
}
743803
}
744804

805+
private boolean _isExplicitlyAnnotatedCreatorPropsBased(PotentialCreator ctor,
806+
Map<String, POJOPropertyBuilder> props, ConstructorDetector ctorDetector)
807+
{
808+
if (ctor.paramCount() == 1) {
809+
// Is ambiguity/heuristics allowed?
810+
switch (ctorDetector.singleArgMode()) {
811+
case DELEGATING:
812+
return false;
813+
case PROPERTIES:
814+
return true;
815+
case REQUIRE_MODE:
816+
throw new IllegalArgumentException(String.format(
817+
"Single-argument constructor (%s) is annotated but no 'mode' defined; `ConstructorDetector`"
818+
+ "configured with `SingleArgConstructor.REQUIRE_MODE`",
819+
ctor.creator()));
820+
case HEURISTIC:
821+
default:
822+
}
823+
}
824+
825+
// First: if explicit names found, is Properties-based
826+
ctor.introspectParamNames(_config);
827+
if (ctor.hasExplicitNames()) {
828+
return true;
829+
}
830+
// Second: [databind#3180] @JsonValue indicates delegating
831+
if ((_jsonValueAccessors != null) && !_jsonValueAccessors.isEmpty()) {
832+
return false;
833+
}
834+
if (ctor.paramCount() == 1) {
835+
// One more possibility: implicit name that maps to implied
836+
// property with at least one visible accessor
837+
String implName = ctor.implicitNameSimple(0);
838+
if (implName != null) {
839+
POJOPropertyBuilder prop = props.get(implName);
840+
if ((prop != null) && prop.anyVisible() && !prop.anyIgnorals()) {
841+
return true;
842+
}
843+
}
844+
// Second: injectable also suffices
845+
if ((_annotationIntrospector != null)
846+
&& _annotationIntrospector.findInjectableValue(_config, ctor.param(0)) != null) {
847+
return true;
848+
}
849+
return false;
850+
}
851+
// Trickiest case: rely on existence of implicit names and/or injectables
852+
return ctor.hasNameOrInjectForAllParams(_config);
853+
}
854+
745855
private void _addCreatorsWithAnnotatedNames(PotentialCreators collector,
746856
List<PotentialCreator> ctors)
747857
{
@@ -760,6 +870,55 @@ private void _addCreatorsWithAnnotatedNames(PotentialCreators collector,
760870
}
761871
}
762872

873+
private boolean _addImplicitConstructor(PotentialCreators collector,
874+
List<PotentialCreator> ctors, Map<String, POJOPropertyBuilder> props)
875+
{
876+
// Must have one and only one candidate
877+
if (ctors.size() != 1) {
878+
return false;
879+
}
880+
final PotentialCreator ctor = ctors.get(0);
881+
// which needs to be visible
882+
final boolean visible = (ctor.paramCount() == 1)
883+
? _visibilityChecker.isScalarConstructorVisible(ctor.creator())
884+
: _visibilityChecker.isCreatorVisible(ctor.creator());
885+
if (!visible) {
886+
return false;
887+
}
888+
ctor.introspectParamNames(_config);
889+
890+
// As usual, 1-param case is distinct
891+
if (ctor.paramCount() != 1) {
892+
if (!ctor.hasNameOrInjectForAllParams(_config)) {
893+
return false;
894+
}
895+
} else {
896+
// First things first: if only param has Injectable, must be Props-based
897+
if ((_annotationIntrospector != null)
898+
&& _annotationIntrospector.findInjectableValue(_config, ctor.param(0)) != null) {
899+
// props-based, continue
900+
} else {
901+
// may have explicit preference
902+
final ConstructorDetector ctorDetector = _config.getConstructorDetector();
903+
if (ctorDetector.singleArgCreatorDefaultsToDelegating()) {
904+
return false;
905+
}
906+
// if not, prefer Properties-based if explicit preference OR
907+
// property with same name with at least one visible accessor
908+
if (!ctorDetector.singleArgCreatorDefaultsToProperties()) {
909+
POJOPropertyBuilder prop = props.get(ctor.implicitNameSimple(0));
910+
if ((prop == null) || !prop.anyVisible() || prop.anyIgnorals()) {
911+
return false;
912+
}
913+
}
914+
}
915+
}
916+
917+
ctors.remove(0);
918+
collector.setPropertiesBased(_config, ctor, "implicit");
919+
return true;
920+
}
921+
763922
private void _addCreatorParams(Map<String, POJOPropertyBuilder> props,
764923
PotentialCreator ctor, List<POJOPropertyBuilder> creatorProps)
765924
{

src/test-jdk17/java/tools/jackson/databind/records/RecordExplicitCreatorsTest.java

+7-10
Original file line numberDiff line numberDiff line change
@@ -199,18 +199,15 @@ public void testDeserializeUsingCanonicalConstructor_WhenJsonCreatorConstructorE
199199
}
200200
}
201201

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

216213
/*

src/test/java/tools/jackson/databind/deser/creators/ConstructorDetector1498Test.java

+8
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@
99
import tools.jackson.databind.*;
1010
import tools.jackson.databind.cfg.*;
1111
import tools.jackson.databind.exc.InvalidDefinitionException;
12+
import tools.jackson.databind.exc.UnrecognizedPropertyException;
1213
import tools.jackson.databind.introspect.AnnotatedMember;
1314
import tools.jackson.databind.introspect.JacksonAnnotationIntrospector;
1415
import tools.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
/*

src/test/java/tools/jackson/databind/deser/creators/SingleArgCreatorTest.java

+6-1
Original file line numberDiff line numberDiff line change
@@ -183,7 +183,12 @@ public void testSingleStringArgWithImplicitName() throws Exception
183183
final ObjectMapper mapper = jsonMapperBuilder()
184184
.annotationIntrospector(new MyParamIntrospector("value"))
185185
.build();
186-
StringyBean bean = mapper.readValue(q("foobar"), StringyBean.class);
186+
// 23-May-2024, tatu: [databind#4515] Clarifies handling to make
187+
// 1-param Constructor with implicit name auto-discoverable
188+
// This is compatibility change so hopefully won't bite us but...
189+
// it seems like the right thing to do.
190+
// StringyBean bean = mapper.readValue(q("foobar"), StringyBean.class);
191+
StringyBean bean = mapper.readValue(a2q("{'value':'foobar'}"), StringyBean.class);
187192
assertEquals("foobar", bean.getValue());
188193
}
189194

src/test/java/tools/jackson/databind/deser/creators/TestCreators.java

+2-4
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,6 @@
44
import java.math.BigInteger;
55
import java.util.*;
66

7-
import org.junit.jupiter.api.Disabled;
87
import org.junit.jupiter.api.Test;
98

109
import com.fasterxml.jackson.annotation.*;
@@ -61,14 +60,14 @@ protected BooleanConstructorBean2(boolean b) {
6160
}
6261

6362
static class DoubleConstructorBean {
64-
Double d; // cup?
63+
Double d;
6564
@JsonCreator protected DoubleConstructorBean(Double d) {
6665
this.d = d;
6766
}
6867
}
6968

7069
static class FactoryBean {
71-
double d; // teehee
70+
double d;
7271

7372
private FactoryBean(double value, boolean dummy) { d = value; }
7473

@@ -413,7 +412,6 @@ public void testStringFactoryAlt() throws Exception
413412
// handling seems inconsistent wrt Constructor/Factory precedence,
414413
// will tackle at a later point -- this is the last JDK8 fail
415414
@Test
416-
@Disabled
417415
public void testConstructorAndFactoryCreator() throws Exception
418416
{
419417
CreatorBeanWithBoth bean = MAPPER.readValue

0 commit comments

Comments
 (0)