From a28cdedd44548c8a008b91496f7fdbd84b8cc31e Mon Sep 17 00:00:00 2001 From: Tatu Saloranta Date: Fri, 27 Dec 2019 17:03:29 -0800 Subject: [PATCH] Fix #2527 --- release-notes/VERSION-2.x | 2 + .../introspect/POJOPropertiesCollector.java | 66 +++++++++++++++---- .../failing/IsGetterRenaming2527Test.java | 42 +++++++++--- 3 files changed, 88 insertions(+), 22 deletions(-) diff --git a/release-notes/VERSION-2.x b/release-notes/VERSION-2.x index 69789431ba..0ae5241073 100644 --- a/release-notes/VERSION-2.x +++ b/release-notes/VERSION-2.x @@ -24,6 +24,8 @@ Project: jackson-databind #2522: `DeserializationContext.handleMissingInstantiator()` throws `MismatchedInputException` for non-static inner classes #2525: Incorrect `JsonStreamContext` for `TokenBuffer` and `TreeTraversingParser` +#2527: Add `AnnotationIntrospector.findRenameByField()` to support Kotlin's "is-getter" + naming convention #2555: Use `@JsonProperty(index)` for sorting properties on serialization #2565: Java 8 `Optional` not working with `@JsonUnwrapped` on unwrappable type (reported by Haowei W) 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 d124ab6404..eee4b52fb5 100644 --- a/src/main/java/com/fasterxml/jackson/databind/introspect/POJOPropertiesCollector.java +++ b/src/main/java/com/fasterxml/jackson/databind/introspect/POJOPropertiesCollector.java @@ -89,6 +89,22 @@ public class POJOPropertiesCollector protected LinkedHashMap _properties; protected LinkedList _creatorProperties; + + /** + * A set of "field renamings" that have been discovered, indicating + * intended renaming of other accesors: key is the implicit original + * name and value intended name to use instead. + *

+ * Note that these renamings are applied earlier than "regular" (explicit) + * renamings and affect implicit name: their effect may be changed by + * further renaming based on explicit indicators. + * The main use case is to effectively relink accessors based on fields + * discovered, and used to sort of correct otherwise missing linkage between + * fields and other accessors. + * + * @since 2.11 + */ + protected Map _fieldRenameMappings; protected LinkedList _anyGetters; @@ -305,7 +321,7 @@ protected void collectAll() LinkedHashMap props = new LinkedHashMap(); // First: gather basic data - _addFields(props); + _addFields(props); // note: populates _fieldRenameMappings _addMethods(props); // 25-Jan-2016, tatu: Avoid introspecting (constructor-)creators for non-static // inner classes, see [databind#1502] @@ -314,9 +330,6 @@ protected void collectAll() } _addInjectables(props); - // 27-Dec-2019, tatu: [databind#2527] initial re-linking by Field needs to - // be applied before other processing - // Remove ignored properties, first; this MUST precede annotation merging // since logic relies on knowing exactly which accessor has which annotation _removeUnwantedProperties(props); @@ -397,15 +410,20 @@ protected void _addFields(Map props) if (implName == null) { implName = f.getName(); } + final PropertyName implNameP = _propNameFromSimple(implName); // [databind#2527: Field-based renaming can be applied early (here), // or at a later point, but probably must be done before pruning // final fields. So let's do it early here - final PropertyName rename = ai.findRenameByField(_config, f, _propNameFromSimple(implName)); - if (rename != null) { + final PropertyName rename = ai.findRenameByField(_config, f, implNameP); + if ((rename != null) && !rename.equals(implNameP)) { + if (_fieldRenameMappings == null) { + _fieldRenameMappings = new HashMap<>(); + } + _fieldRenameMappings.put(rename, implNameP); // todo } - + PropertyName pn; if (_forSerialization) { @@ -511,9 +529,12 @@ protected void _addCreatorParam(Map props, pn = PropertyName.construct(impl); } + // 27-Dec-2019, tatu: [databind#2527] may need to rename according to field + impl = _checkRenameByField(impl); + // shouldn't need to worry about @JsonIgnore, since creators only added // if so annotated - + /* 13-May-2015, tatu: We should try to start with implicit name, similar to how * fields and methods work; but unlike those, we don't necessarily have * implicit name to use (pre-Java8 at least). So: @@ -531,11 +552,11 @@ protected void _addMethods(Map props) { final AnnotationIntrospector ai = _annotationIntrospector; for (AnnotatedMethod m : _classDef.memberMethods()) { - /* For methods, handling differs between getters and setters; and - * we will also only consider entries that either follow the bean - * naming convention or are explicitly marked: just being visible - * is not enough (unlike with fields) - */ + // For methods, handling differs between getters and setters; and + // we will also only consider entries that either follow the bean + // naming convention or are explicitly marked: just being visible + // is not enough (unlike with fields) + int argCount = m.getParameterCount(); if (argCount == 0) { // getters (including 'any getter') _addGetterMethod(props, m, ai); @@ -616,6 +637,8 @@ protected void _addGetterMethod(Map props, } visible = true; } + // 27-Dec-2019, tatu: [databind#2527] may need to rename according to field + implName = _checkRenameByField(implName); boolean ignore = ai.hasIgnoreMarker(m); _property(props, implName).addGetter(m, pn, nameExplicit, visible, ignore); } @@ -653,6 +676,8 @@ protected void _addSetterMethod(Map props, } visible = true; } + // 27-Dec-2019, tatu: [databind#2527] may need to rename according to field + implName = _checkRenameByField(implName); boolean ignore = (ai == null) ? false : ai.hasIgnoreMarker(m); _property(props, implName).addSetter(m, pn, nameExplicit, visible, ignore); } @@ -697,7 +722,20 @@ protected void _doAddInjectable(JacksonInject.Value injectable, AnnotatedMember private PropertyName _propNameFromSimple(String simpleName) { return PropertyName.construct(simpleName, null); } - + + // @since 2.11 + private String _checkRenameByField(String implName) { + if (_fieldRenameMappings != null) { + PropertyName p = _fieldRenameMappings.get(_propNameFromSimple(implName)); + if (p != null) { + implName = p.getSimpleName(); + return implName; + + } + } + return implName; + } + /* /********************************************************** /* Internal methods; removing ignored properties diff --git a/src/test/java/com/fasterxml/jackson/failing/IsGetterRenaming2527Test.java b/src/test/java/com/fasterxml/jackson/failing/IsGetterRenaming2527Test.java index 78c62da8bc..b81d9fcbb1 100644 --- a/src/test/java/com/fasterxml/jackson/failing/IsGetterRenaming2527Test.java +++ b/src/test/java/com/fasterxml/jackson/failing/IsGetterRenaming2527Test.java @@ -3,6 +3,7 @@ import java.util.Collections; import java.util.Map; +import com.fasterxml.jackson.annotation.JsonProperty; import com.fasterxml.jackson.databind.*; import com.fasterxml.jackson.databind.cfg.MapperConfig; import com.fasterxml.jackson.databind.introspect.AnnotatedField; @@ -24,18 +25,28 @@ public POJO2527(boolean b) { public void setEnabled(boolean b) { isEnabled = b; } } - static class POJO2527b { + static class POJO2527PublicField { public boolean isEnabled; - protected POJO2527b() { } - public POJO2527b(boolean b) { + protected POJO2527PublicField() { } + public POJO2527PublicField(boolean b) { isEnabled = b; } public boolean getEnabled() { return isEnabled; } public void setEnabled(boolean b) { isEnabled = b; } } - + + static class POJO2527Creator { + private final boolean isEnabled; + + public POJO2527Creator(@JsonProperty("enabled") boolean b) { + isEnabled = b; + } + + public boolean getEnabled() { return isEnabled; } + } + @SuppressWarnings("serial") static class MyIntrospector extends JacksonAnnotationIntrospector { @@ -55,7 +66,9 @@ public PropertyName findRenameByField(MapperConfig config, } } - private final ObjectMapper MAPPER = newJsonMapper(); + private final ObjectMapper MAPPER = jsonMapperBuilder() + .annotationIntrospector(new MyIntrospector()) + .build(); public void testIsPropertiesStdKotlin() throws Exception { @@ -70,16 +83,29 @@ public void testIsPropertiesStdKotlin() throws Exception assertEquals(input.isEnabled, output.isEnabled); } - public void testIsPropertiesAlt() throws Exception + public void testIsPropertiesWithPublicField() throws Exception + { + POJO2527PublicField input = new POJO2527PublicField(true); + final String json = MAPPER.writeValueAsString(input); + + Map props = MAPPER.readValue(json, Map.class); + assertEquals(Collections.singletonMap("isEnabled", Boolean.TRUE), + props); + + POJO2527PublicField output = MAPPER.readValue(json, POJO2527PublicField.class); + assertEquals(input.isEnabled, output.isEnabled); + } + + public void testIsPropertiesViaCreator() throws Exception { - POJO2527b input = new POJO2527b(true); + POJO2527Creator input = new POJO2527Creator(true); final String json = MAPPER.writeValueAsString(input); Map props = MAPPER.readValue(json, Map.class); assertEquals(Collections.singletonMap("isEnabled", Boolean.TRUE), props); - POJO2527b output = MAPPER.readValue(json, POJO2527b.class); + POJO2527Creator output = MAPPER.readValue(json, POJO2527Creator.class); assertEquals(input.isEnabled, output.isEnabled); } }