Skip to content

Commit 6e68d81

Browse files
authored
Fixes #4584: add AnnotationIntrospector method for primary Creator discovery (#4615)
1 parent bed9064 commit 6e68d81

File tree

6 files changed

+433
-59
lines changed

6 files changed

+433
-59
lines changed

release-notes/VERSION-2.x

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -50,6 +50,8 @@ Project: jackson-databind
5050
#4570: Deprecate `ObjectMapper.canDeserialize()`/`ObjectMapper.canSerialize()`
5151
#4580: Add `MapperFeature.SORT_CREATOR_PROPERTIES_BY_DECLARATION_ORDER` to use
5252
Creator properties' declaration order for sorting
53+
#4584: Provide extension point for detecting "primary" Constructor for Kotlin
54+
(and similar) data classes
5355
#4602: Possible wrong use of _arrayDelegateDeserializer in
5456
BeanDeserializerBase::deserializeFromObjectUsingNonDefault()
5557
(reported by Eduard G)

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

Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1397,6 +1397,39 @@ public JsonCreator.Mode findCreatorAnnotation(MapperConfig<?> config, Annotated
13971397
return null;
13981398
}
13991399

1400+
/**
1401+
* Method called to check if introspector is able to detect so-called Primary
1402+
* Creator: Creator to select for use when no explicit annotation is found
1403+
* (via {@link #findCreatorAnnotation}).
1404+
* This is the case for example for Java Record types which have so-called
1405+
* canonical constructor; but it is also true for various "Data" classes by frameworks
1406+
* like Lombok and JVM languages like Kotlin and Scala (case classes).
1407+
* If introspector can determine that one of given {@link PotentialCreator}s should
1408+
* be considered Primary, it should return it; if not, should return {@code null}.
1409+
*<p>
1410+
* NOTE: when returning chosen Creator, it may be necessary to mark its "mode"
1411+
* with {@link PotentialCreator#overrideMode} (especially for "delegating" creators).
1412+
*<p>
1413+
* NOTE: method is NOT called for Java Record types; selection of the canonical constructor
1414+
* as the Primary creator is handled directly by {@link POJOPropertiesCollector}
1415+
*
1416+
* @param config Configuration settings in effect (for deserialization)
1417+
* @param valueClass Class being instantiated and defines Creators passed
1418+
* @param declaredConstructors Constructors value class declares
1419+
* @param declaredFactories Factory methods value class declares
1420+
*
1421+
* @return The one Canonical Creator to use for {@code valueClass}, if it can be
1422+
* determined; {@code null} if not.
1423+
*
1424+
* @since 2.18
1425+
*/
1426+
public PotentialCreator findPrimaryCreator(MapperConfig<?> config,
1427+
AnnotatedClass valueClass,
1428+
List<PotentialCreator> declaredConstructors,
1429+
List<PotentialCreator> declaredFactories) {
1430+
return null;
1431+
}
1432+
14001433
/**
14011434
* Method for checking whether given annotated item (method, constructor)
14021435
* has an annotation

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

Lines changed: 15 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -735,9 +735,23 @@ public JsonCreator.Mode findCreatorAnnotation(MapperConfig<?> config, Annotated
735735
return (mode == null) ? _secondary.findCreatorAnnotation(config, a) : mode;
736736
}
737737

738+
@Override
739+
public PotentialCreator findPrimaryCreator(MapperConfig<?> config,
740+
AnnotatedClass valueClass,
741+
List<PotentialCreator> declaredConstructors,
742+
List<PotentialCreator> declaredFactories) {
743+
PotentialCreator primary = _primary.findPrimaryCreator(config,
744+
valueClass, declaredConstructors, declaredFactories);
745+
if (primary == null) {
746+
primary = _secondary.findPrimaryCreator(config,
747+
valueClass, declaredConstructors, declaredFactories);
748+
}
749+
return primary;
750+
}
751+
738752
/*
739753
/**********************************************************************
740-
/* Deserialization: other method annotations
754+
/* Deserialization: other property annotations
741755
/**********************************************************************
742756
*/
743757

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

Lines changed: 39 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -648,22 +648,22 @@ protected void _addCreators(Map<String, POJOPropertyBuilder> props)
648648
List<PotentialCreator> constructors = _collectCreators(_classDef.getConstructors());
649649
List<PotentialCreator> factories = _collectCreators(_classDef.getFactoryMethods());
650650

651-
final PotentialCreator canonical;
652-
653-
// Find and mark "canonical" constructor for Records.
651+
// Then find what is the Primary Constructor (if one exists for type):
652+
// for Java Records and potentially other types too ("data classes"):
654653
// Needs to be done early to get implicit names populated
654+
final PotentialCreator primary;
655655
if (_isRecordType) {
656-
canonical = JDK14Util.findCanonicalRecordConstructor(_config, _classDef, constructors);
656+
primary = JDK14Util.findCanonicalRecordConstructor(_config, _classDef, constructors);
657657
} else {
658-
// !!! TODO: fetch Canonical for Kotlin, Scala, via AnnotationIntrospector?
659-
canonical = null;
658+
primary = _annotationIntrospector.findPrimaryCreator(_config, _classDef,
659+
constructors, factories);
660660
}
661-
662661
// Next: remove creators marked as explicitly disabled
663662
_removeDisabledCreators(constructors);
664663
_removeDisabledCreators(factories);
664+
665665
// And then remove non-annotated static methods that do not look like factories
666-
_removeNonFactoryStaticMethods(factories);
666+
_removeNonFactoryStaticMethods(factories, primary);
667667

668668
// and use annotations to find explicitly chosen Creators
669669
if (_useAnnotations) { // can't have explicit ones without Annotation introspection
@@ -681,18 +681,18 @@ protected void _addCreators(Map<String, POJOPropertyBuilder> props)
681681
_addCreatorsWithAnnotatedNames(creators, constructors);
682682
}
683683

684-
// But if no annotation-based Creators found, find/use canonical Creator
685-
// (JDK 17 Record/Scala/Kotlin)
686-
if (!creators.hasPropertiesBased()) {
687-
// for Records:
688-
if (canonical != null) {
684+
// But if no annotation-based Creators found, find/use Primary Creator
685+
// detected earlier, if any
686+
if (primary != null) {
687+
if (!creators.hasPropertiesBased()) {
689688
// ... but only process if still included as a candidate
690-
if (constructors.remove(canonical)) {
689+
if (constructors.remove(primary)
690+
|| factories.remove(primary)) {
691691
// But wait! Could be delegating
692-
if (_isDelegatingConstructor(canonical)) {
693-
creators.addExplicitDelegating(canonical);
692+
if (_isDelegatingConstructor(primary)) {
693+
creators.addExplicitDelegating(primary);
694694
} else {
695-
creators.setPropertiesBased(_config, canonical, "canonical");
695+
creators.setPropertiesBased(_config, primary, "Primary");
696696
}
697697
}
698698
}
@@ -720,19 +720,29 @@ protected void _addCreators(Map<String, POJOPropertyBuilder> props)
720720

721721
// And finally add logical properties for the One Properties-based
722722
// creator selected (if any):
723-
PotentialCreator primary = creators.propertiesBased;
724-
if (primary == null) {
723+
PotentialCreator propsCtor = creators.propertiesBased;
724+
if (propsCtor == null) {
725725
_creatorProperties = Collections.emptyList();
726726
} else {
727727
_creatorProperties = new ArrayList<>();
728-
_addCreatorParams(props, primary, _creatorProperties);
728+
_addCreatorParams(props, propsCtor, _creatorProperties);
729729
}
730730
}
731731

732732
// Method to determine if given non-explictly-annotated constructor
733733
// looks like delegating one
734734
private boolean _isDelegatingConstructor(PotentialCreator ctor)
735735
{
736+
// First things first: could be
737+
switch (ctor.creatorModeOrDefault()) {
738+
case DELEGATING:
739+
return true;
740+
case DISABLED:
741+
case PROPERTIES:
742+
return false;
743+
default: // case DEFAULT:
744+
}
745+
736746
// Only consider single-arg case, for now
737747
if (ctor.paramCount() == 1) {
738748
// Main thing: @JsonValue makes it delegating:
@@ -752,6 +762,7 @@ private List<PotentialCreator> _collectCreators(List<? extends AnnotatedWithPara
752762
for (AnnotatedWithParams ctor : ctors) {
753763
JsonCreator.Mode creatorMode = _useAnnotations
754764
? _annotationIntrospector.findCreatorAnnotation(_config, ctor) : null;
765+
// 06-Jul-2024, tatu: Can't yet drop DISABLED ones; add all (for now)
755766
result.add(new PotentialCreator(ctor, creatorMode));
756767
}
757768
return (result == null) ? Collections.emptyList() : result;
@@ -779,14 +790,19 @@ private void _removeNonVisibleCreators(List<PotentialCreator> ctors)
779790
}
780791
}
781792

782-
private void _removeNonFactoryStaticMethods(List<PotentialCreator> ctors)
793+
private void _removeNonFactoryStaticMethods(List<PotentialCreator> ctors,
794+
PotentialCreator canonical)
783795
{
784796
final Class<?> rawType = _type.getRawClass();
785797
Iterator<PotentialCreator> it = ctors.iterator();
786798
while (it.hasNext()) {
787799
// explicit mode? Retain (for now)
788800
PotentialCreator ctor = it.next();
789-
if (ctor.creatorMode() != null) {
801+
if (ctor.isAnnotated()) {
802+
continue;
803+
}
804+
// Do not trim canonical either
805+
if (canonical == ctor) {
790806
continue;
791807
}
792808
// Copied from `BasicBeanDescription.isFactoryMethod()`
@@ -820,7 +836,7 @@ private void _addExplicitlyAnnotatedCreators(PotentialCreators collector,
820836

821837
// If no explicit annotation, skip for now (may be discovered
822838
// at a later point)
823-
if (ctor.creatorMode() == null) {
839+
if (!ctor.isAnnotated()) {
824840
continue;
825841
}
826842

0 commit comments

Comments
 (0)