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 2e1bc148c0..8ad07a53a8 100644 --- a/src/main/java/com/fasterxml/jackson/databind/introspect/POJOPropertiesCollector.java +++ b/src/main/java/com/fasterxml/jackson/databind/introspect/POJOPropertiesCollector.java @@ -501,6 +501,40 @@ protected void collectAll() _collected = true; } + /** + * [databind#5215] JsonAnyGetter Serializer behavior change from 2.18.4 to 2.19.0 + * Put anyGetter in the end, before actual sorting further down {@link POJOPropertiesCollector#_sortProperties(Map)} + */ + private Map _putAnyGettersInTheEnd( + Map sortedProps) + { + AnnotatedMember anyAccessor; + + if (_anyGetters != null) { + anyAccessor = _anyGetters.getFirst(); + } else if (_anyGetterField != null) { + anyAccessor = _anyGetterField.getFirst(); + } else { + return sortedProps; + } + + // Here we'll use insertion-order preserving map, since possible alphabetic + // sorting already done earlier + Map newAll = new LinkedHashMap<>(sortedProps.size() * 2); + POJOPropertyBuilder anyGetterProp = null; + for (POJOPropertyBuilder prop : sortedProps.values()) { + if (prop.hasFieldOrGetter(anyAccessor)) { + anyGetterProp = prop; + } else { + newAll.put(prop.getName(), prop); + } + } + if (anyGetterProp != null) { + newAll.put(anyGetterProp.getName(), anyGetterProp); + } + return newAll; + } + /* /********************************************************************** /* Property introspection: Fields @@ -1588,14 +1622,16 @@ protected void _sortProperties(Map props) Map all; // Need to (re)sort alphabetically? if (sortAlpha) { - all = new TreeMap(); + all = new TreeMap<>(); } else { - all = new LinkedHashMap(size+size); + all = new LinkedHashMap<>(size+size); } - + // First, handle sorting caller expects: for (POJOPropertyBuilder prop : props.values()) { all.put(prop.getName(), prop); } + all = _putAnyGettersInTheEnd(all); + Map ordered = new LinkedHashMap<>(size+size); // Ok: primarily by explicit order if (propertyOrder != null) { diff --git a/src/main/java/com/fasterxml/jackson/databind/introspect/POJOPropertyBuilder.java b/src/main/java/com/fasterxml/jackson/databind/introspect/POJOPropertyBuilder.java index e0c6f497df..088a7f8b30 100644 --- a/src/main/java/com/fasterxml/jackson/databind/introspect/POJOPropertyBuilder.java +++ b/src/main/java/com/fasterxml/jackson/databind/introspect/POJOPropertyBuilder.java @@ -1,5 +1,6 @@ package com.fasterxml.jackson.databind.introspect; +import java.lang.reflect.Member; import java.util.*; import java.util.stream.Collectors; @@ -764,6 +765,24 @@ protected int _setterPriority(AnnotatedMethod m) return 2; } + // @since 2.19.2 + public boolean hasFieldOrGetter(AnnotatedMember member) { + return _hasAccessor(_fields, member) || _hasAccessor(_getters, member); + } + + private boolean _hasAccessor(Linked node, + AnnotatedMember memberToMatch) + { + // AnnotatedXxx are not canonical, but underlying JDK Members are: + final Member rawMemberToMatch = memberToMatch.getMember(); + for (; node != null; node = node.next) { + if (node.value.getMember() == rawMemberToMatch) { + return true; + } + } + return false; + } + /* /********************************************************** /* Implementations of refinement accessors @@ -877,19 +896,19 @@ public JsonProperty.Access withMember(AnnotatedMember member) { */ public void addField(AnnotatedField a, PropertyName name, boolean explName, boolean visible, boolean ignored) { - _fields = new Linked(a, _fields, name, explName, visible, ignored); + _fields = new Linked<>(a, _fields, name, explName, visible, ignored); } public void addCtor(AnnotatedParameter a, PropertyName name, boolean explName, boolean visible, boolean ignored) { - _ctorParameters = new Linked(a, _ctorParameters, name, explName, visible, ignored); + _ctorParameters = new Linked<>(a, _ctorParameters, name, explName, visible, ignored); } public void addGetter(AnnotatedMethod a, PropertyName name, boolean explName, boolean visible, boolean ignored) { - _getters = new Linked(a, _getters, name, explName, visible, ignored); + _getters = new Linked<>(a, _getters, name, explName, visible, ignored); } public void addSetter(AnnotatedMethod a, PropertyName name, boolean explName, boolean visible, boolean ignored) { - _setters = new Linked(a, _setters, name, explName, visible, ignored); + _setters = new Linked<>(a, _setters, name, explName, visible, ignored); } /** diff --git a/src/test-jdk17/java/com/fasterxml/jackson/databind/records/RecordJsonSerDeser188Test.java b/src/test-jdk17/java/com/fasterxml/jackson/databind/records/RecordJsonSerDeser188Test.java index 73e6c14d3d..6407074c59 100644 --- a/src/test-jdk17/java/com/fasterxml/jackson/databind/records/RecordJsonSerDeser188Test.java +++ b/src/test-jdk17/java/com/fasterxml/jackson/databind/records/RecordJsonSerDeser188Test.java @@ -48,7 +48,6 @@ public void serialize(String value, JsonGenerator jgen, SerializerProvider provi } } - @SuppressWarnings("serial") static class PrefixStringDeserializer extends StdScalarDeserializer { private static final long serialVersionUID = 1L; diff --git a/src/test/java/com/fasterxml/jackson/databind/ser/AnyGetterOrdering5215Test.java b/src/test/java/com/fasterxml/jackson/databind/ser/AnyGetterOrdering5215Test.java new file mode 100644 index 0000000000..0e38fca74a --- /dev/null +++ b/src/test/java/com/fasterxml/jackson/databind/ser/AnyGetterOrdering5215Test.java @@ -0,0 +1,67 @@ +package com.fasterxml.jackson.databind.ser; + +import com.fasterxml.jackson.annotation.JsonAnyGetter; +import com.fasterxml.jackson.annotation.JsonAnySetter; +import com.fasterxml.jackson.databind.MapperFeature; +import com.fasterxml.jackson.databind.ObjectMapper; +import com.fasterxml.jackson.databind.SerializationFeature; +import com.fasterxml.jackson.databind.json.JsonMapper; +import com.fasterxml.jackson.databind.testutil.DatabindTestUtil; + +import org.junit.jupiter.api.Test; + +import java.util.LinkedHashMap; +import java.util.Map; + +import static org.junit.jupiter.api.Assertions.assertEquals; + +// For [databind#5215]: Any-getter should be sorted last, by default +public class AnyGetterOrdering5215Test + extends DatabindTestUtil +{ + static class DynaBean { + public String l; + public String j; + public String a; + + protected Map extensions = new LinkedHashMap<>(); + + @JsonAnyGetter + public Map getExtensions() { + return extensions; + } + + @JsonAnySetter + public void addExtension(String name, Object value) { + extensions.put(name, value); + } + } + + /* + /********************************************************************** + /* Test methods + /********************************************************************** + */ + + private final ObjectMapper MAPPER = JsonMapper.builder() + .enable(MapperFeature.SORT_PROPERTIES_ALPHABETICALLY) + .configure(SerializationFeature.ORDER_MAP_ENTRIES_BY_KEYS, true) + .build(); + + @Test + public void testDynaBean() throws Exception + { + DynaBean b = new DynaBean(); + b.a = "1"; + b.j = "2"; + b.l = "3"; + b.addExtension("z", "5"); + b.addExtension("b", "4"); + assertEquals(a2q("{" + + "'a':'1'," + + "'j':'2'," + + "'l':'3'," + + "'b':'4'," + + "'z':'5'}"), MAPPER.writeValueAsString(b)); + } +}