Skip to content

Fix serialization order change after #4775 (@JsonAnyGetter respects order) #5216

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 7 commits into from
Jul 9, 2025
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -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<String, POJOPropertyBuilder> _putAnyGettersInTheEnd(
Map<String, POJOPropertyBuilder> 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<String, POJOPropertyBuilder> 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
Expand Down Expand Up @@ -1588,14 +1622,16 @@ protected void _sortProperties(Map<String, POJOPropertyBuilder> props)
Map<String, POJOPropertyBuilder> all;
// Need to (re)sort alphabetically?
if (sortAlpha) {
all = new TreeMap<String,POJOPropertyBuilder>();
all = new TreeMap<>();
} else {
all = new LinkedHashMap<String,POJOPropertyBuilder>(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<String,POJOPropertyBuilder> ordered = new LinkedHashMap<>(size+size);
// Ok: primarily by explicit order
if (propertyOrder != null) {
Expand Down
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
package com.fasterxml.jackson.databind.introspect;

import java.lang.reflect.Member;
import java.util.*;
import java.util.stream.Collectors;

Expand Down Expand Up @@ -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<? extends AnnotatedMember> 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
Expand Down Expand Up @@ -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<AnnotatedField>(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<AnnotatedParameter>(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<AnnotatedMethod>(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<AnnotatedMethod>(a, _setters, name, explName, visible, ignored);
_setters = new Linked<>(a, _setters, name, explName, visible, ignored);
}

/**
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -48,7 +48,6 @@ public void serialize(String value, JsonGenerator jgen, SerializerProvider provi
}
}

@SuppressWarnings("serial")
static class PrefixStringDeserializer extends StdScalarDeserializer<String>
{
private static final long serialVersionUID = 1L;
Expand Down
Original file line number Diff line number Diff line change
@@ -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<String, Object> extensions = new LinkedHashMap<>();

@JsonAnyGetter
public Map<String, Object> 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));
}
}