diff --git a/release-notes/CREDITS-2.x b/release-notes/CREDITS-2.x index 9ade67f5d7..8a24cdb78e 100644 --- a/release-notes/CREDITS-2.x +++ b/release-notes/CREDITS-2.x @@ -1532,3 +1532,7 @@ Moritz Halbritter (mhalbritter@github) Philippe Marschall (marschall@github) * Contributed #3699: Allow custom `JsonNode` implementations (2.14.2) + +Gili Tzabari (cowwoc@github) + * Reported #3063: `@JsonValue` fails for Java Record + (2.14.2) diff --git a/release-notes/VERSION-2.x b/release-notes/VERSION-2.x index 18f122d5da..d447bbcf58 100644 --- a/release-notes/VERSION-2.x +++ b/release-notes/VERSION-2.x @@ -8,6 +8,8 @@ Project: jackson-databind #1751: `@JsonTypeInfo` does not work if the Type Id is an Integer value (reported by @marvin-we) +#3063: `@JsonValue` fails for Java Record + (reported by Gili T) #3699: Allow custom `JsonNode` implementations (contributed by Philippe M) #3711: Enum polymorphism not working correctly with DEDUCTION diff --git a/src/main/java/com/fasterxml/jackson/databind/introspect/POJOPropertiesCollector.java b/src/main/java/com/fasterxml/jackson/databind/introspect/POJOPropertiesCollector.java index 1fefc16495..037ec94139 100644 --- a/src/main/java/com/fasterxml/jackson/databind/introspect/POJOPropertiesCollector.java +++ b/src/main/java/com/fasterxml/jackson/databind/introspect/POJOPropertiesCollector.java @@ -251,9 +251,11 @@ public AnnotatedMember getJsonKeyAccessor() { // If @JsonKey defined, must have a single one if (_jsonKeyAccessors != null) { if (_jsonKeyAccessors.size() > 1) { - reportProblem("Multiple 'as-key' properties defined (%s vs %s)", - _jsonKeyAccessors.get(0), - _jsonKeyAccessors.get(1)); + if (!_resolveFieldVsGetter(_jsonKeyAccessors)) { + reportProblem("Multiple 'as-key' properties defined (%s vs %s)", + _jsonKeyAccessors.get(0), + _jsonKeyAccessors.get(1)); + } } // otherwise we won't greatly care return _jsonKeyAccessors.get(0); @@ -270,11 +272,14 @@ public AnnotatedMember getJsonValueAccessor() collectAll(); } // If @JsonValue defined, must have a single one + // 15-Jan-2023, tatu: Except let's try resolving "getter-over-field" case at least if (_jsonValueAccessors != null) { if (_jsonValueAccessors.size() > 1) { - reportProblem("Multiple 'as-value' properties defined (%s vs %s)", - _jsonValueAccessors.get(0), - _jsonValueAccessors.get(1)); + if (!_resolveFieldVsGetter(_jsonValueAccessors)) { + reportProblem("Multiple 'as-value' properties defined (%s vs %s)", + _jsonValueAccessors.get(0), + _jsonValueAccessors.get(1)); + } } // otherwise we won't greatly care return _jsonValueAccessors.get(0); @@ -1123,7 +1128,7 @@ protected void _renameWithWrappers(Map props) /* /********************************************************** - /* Overridable internal methods, sorting, other stuff + /* Internal methods, sorting /********************************************************** */ @@ -1244,6 +1249,48 @@ private boolean _anyIndexed(Collection props) { return false; } + /* + /********************************************************** + /* Internal methods, conflict resolution + /********************************************************** + */ + + /** + * Method that will be given a {@link List} with 2 or more accessors + * that may be in conflict: it will need to remove lower-priority accessors + * to leave just a single highest-priority accessor to use. + * If this succeeds method returns {@code true}, otherwise {@code false}. + *

+ * NOTE: method will directly modify given {@code List} directly, regardless + * of whether it ultimately succeeds or not. + * + * @return True if seeming conflict was resolved and there only remains + * single accessor + */ + protected boolean _resolveFieldVsGetter(List accessors) { + do { + AnnotatedMember acc1 = accessors.get(0); + AnnotatedMember acc2 = accessors.get(1); + + if (acc1 instanceof AnnotatedField) { + if (acc2 instanceof AnnotatedMethod) { + // Method has precedence, remove first entry + accessors.remove(0); + continue; + } + } else if (acc1 instanceof AnnotatedMethod) { + // Method has precedence, remove second entry + if (acc2 instanceof AnnotatedField) { + accessors.remove(1); + continue; + } + } + // Not a field/method pair; fail + return false; + } while (accessors.size() > 1); + return true; + } + /* /********************************************************** /* Internal methods; helpers diff --git a/src/test-jdk14/java/com/fasterxml/jackson/databind/records/RecordJsonValue3063Test.java b/src/test-jdk14/java/com/fasterxml/jackson/databind/records/RecordJsonValue3063Test.java new file mode 100644 index 0000000000..fe6512f2da --- /dev/null +++ b/src/test-jdk14/java/com/fasterxml/jackson/databind/records/RecordJsonValue3063Test.java @@ -0,0 +1,33 @@ +package com.fasterxml.jackson.databind.records; + +import java.util.Collections; +import java.util.Map; + +import com.fasterxml.jackson.annotation.*; + +import com.fasterxml.jackson.databind.BaseMapTest; +import com.fasterxml.jackson.databind.ObjectMapper; + +public class RecordJsonValue3063Test extends BaseMapTest +{ + // [databind#3063] + record GetLocations3063(@JsonValue Map nameToLocation) + { + @JsonCreator + public GetLocations3063(Map nameToLocation) + { + this.nameToLocation = nameToLocation; + } + } + + private final ObjectMapper MAPPER = newJsonMapper(); + + // [databind#3063] + public void testRecordWithJsonValue3063() throws Exception + { + Map locations = Collections.singletonMap("a", "locationA"); + String json = MAPPER.writeValueAsString(new GetLocations3063(locations)); + + assertNotNull(json); + } +}