You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
Caching. A common pattern is to generate a request body using Gson. Another common pattern is to cache the response by using a hash of the request body as a cache key. Gson does not guarantee stable JSON order for some objects (notably, ImmutableSet/ImmutableMap), leading to non-deterministic cache keys for essentially the same requests.
Testing. If you want to test the serialization of object trees that contain ImmutableSet/ImmutableMap, you can't write string assertions now because Gson output is non-deterministic.
Feature description
new GsonBuilder().sortOutputAlphabetically().create() — serialize all JSON keys alphabetically. Deserializer works as before.
Maybe a better option would be something like: new GsonBuilder().setOutputOrdering(ALPHABETICAL).create()
Alternatives / workarounds
Avoid using ImmutableSet/ImmutableMap. Unfortunately, you often have to serialize other teams' objects (sometimes coming from other libraries), making this recommendation impractical.
Write your TypeAdapters. Hard, error-prone, and time-consuming. I'm still struggling to make it work. Would appreciate an example that enforces Set and Map ordering.
For Set (or generally Collection) it might be a bit more difficult because they are serialized as JSON array and could contain non-String values. The question is how they should be handled.
Also, maybe for better performance users might want to select in which cases this sorting should be performed. If the Set or Map implementation already has a well defined iteration order, then it might be redundant to sort their entries. And the performance overhead is non-negligible (especially when using streaming serialization to a Writer), because all entries have to be collected first to be sorted before they can be written.
Gson does not guarantee stable JSON order for some objects (notably, ImmutableSet/ImmutableMap)
Note that Guava has the subclasses ImmutableSortedSet and ImmutableSortedMap. Gson just serializes the entries in iteration order, so if the Set or Map has a well defined iteration order, then that is preserved.
Side note: Unless you registered custom adapters (either your own, or from an external library) for ImmutableSet/ImmutableMap you might run into issues in Gson 2.13.0, see the release notes. Related: #2742
Workarounds
[...]
Write your TypeAdapters. Hard, error-prone, and time-consuming. I'm still struggling to make it work. Would appreciate an example that enforces Set and Map ordering.
The easiest way to implement this might be to write a TypeAdapterFactory which delegates to your existing Map and Set adapters to create an in-memory JsonElement tree, and afterwards sorts it before writing it.
For example:
(click to expand / collapse)
classSetSortingSerializationFactoryimplementsTypeAdapterFactory {
publicstaticfinalSetSortingSerializationFactoryINSTANCE = newSetSortingSerializationFactory();
privateSetSortingSerializationFactory() {}
@Overridepublic <T> TypeAdapter<T> create(Gsongson, TypeToken<T> type) {
// Only handle Set implementations; for all other types use other registered adapters// Can make this more specific if desired, e.g. by checking `MySet.class` instead of any `Set.class`if (!Set.class.isAssignableFrom(type.getRawType())) {
returnnull;
}
TypeAdapter<JsonElement> jsonElementAdapter = gson.getAdapter(JsonElement.class);
TypeAdapter<T> delegateAdapter = gson.getDelegateAdapter(this, type);
returnnewTypeAdapter<T>() {
// For now only support JSON string valuesprivateStringgetAsString(JsonElementjson) {
JsonPrimitivejsonPrimitive = json.getAsJsonPrimitive();
if (!jsonPrimitive.isString()) {
thrownewIllegalArgumentException("Expected JSON string but was: " + jsonPrimitive);
}
returnjsonPrimitive.getAsString();
}
@Overridepublicvoidwrite(JsonWriterout, Tvalue) throwsIOException {
JsonElementjson = delegateAdapter.toJsonTree(value);
// Can only handle JSON array, leave other JSON values unchangedif (json.isJsonArray()) {
JsonArrayjsonArray = json.getAsJsonArray();
Comparator<String> comparator = Comparator.naturalOrder();
// Sort existing `jsonArray` through its List viewjsonArray.asList().sort(Comparator.comparing(this::getAsString, comparator));
}
jsonElementAdapter.write(out, json);
}
@OverridepublicTread(JsonReaderin) throwsIOException {
returndelegateAdapter.read(in);
}
};
}
}
classMapSortingSerializationFactoryimplementsTypeAdapterFactory {
publicstaticfinalMapSortingSerializationFactoryINSTANCE = newMapSortingSerializationFactory();
privateMapSortingSerializationFactory() {}
@Overridepublic <T> TypeAdapter<T> create(Gsongson, TypeToken<T> type) {
// Only handle Map implementations; for all other types use other registered adapters// Can make this more specific if desired, e.g. by checking `MyMap.class` instead of any `Map.class`if (!Map.class.isAssignableFrom(type.getRawType())) {
returnnull;
}
TypeAdapter<JsonElement> jsonElementAdapter = gson.getAdapter(JsonElement.class);
TypeAdapter<T> delegateAdapter = gson.getDelegateAdapter(this, type);
returnnewTypeAdapter<T>() {
@Overridepublicvoidwrite(JsonWriterout, Tvalue) throwsIOException {
JsonElementjson = delegateAdapter.toJsonTree(value);
// Can only handle JSON object, leave other JSON values unchangedif (json.isJsonObject()) {
JsonObjectjsonObject = json.getAsJsonObject();
List<String> keys = newArrayList<>(jsonObject.keySet());
Comparator<String> comparator = Comparator.naturalOrder();
keys.sort(comparator);
JsonObjectnewObject = newJsonObject();
// Insert sorted entries; JsonObject preserves insertion orderfor (Stringkey : keys) {
newObject.add(key, jsonObject.get(key));
}
json = newObject;
}
jsonElementAdapter.write(out, json);
}
@OverridepublicTread(JsonReaderin) throwsIOException {
returndelegateAdapter.read(in);
}
};
}
}
And then register them like this:
Gsongson = newGsonBuilder()
// Register all other adapters and factories first// ...// Register the sorting factories last, because they delegate to the other adapters
.registerTypeAdapterFactory(SetSortingSerializationFactory.INSTANCE)
.registerTypeAdapterFactory(MapSortingSerializationFactory.INSTANCE)
.create();
Set<String> mySet = newLinkedHashSet<>(Arrays.asList("b", "c", "a"));
System.out.println(gson.toJson(mySet));
Map<String, Object> myMap = newLinkedHashMap<>();
myMap.put("b", 1);
myMap.put("c", Arrays.asList(1, 2));
myMap.put("a", true);
System.out.println(gson.toJson(myMap));
Most likely you want to adjust the Set.class and Map.class checks in the #create methods to be more specific and only handle the Set and Map implementations which really need sorting. Otherwise this decreases performance by redundantly sorting the JSON output for other Set and Map implementations.
(Note: I have not extensively tested this, and maybe it can be refactored to improve performance. But it should work as desired.)
Activity
[-]Sort JSON output alphabetically[/-][+]Option to sort JSON output alphabetically[/+]Marcono1234 commentedon Apr 18, 2025
For
Set(or generallyCollection) it might be a bit more difficult because they are serialized as JSON array and could contain non-String values. The question is how they should be handled.Also, maybe for better performance users might want to select in which cases this sorting should be performed. If the
SetorMapimplementation already has a well defined iteration order, then it might be redundant to sort their entries. And the performance overhead is non-negligible (especially when using streaming serialization to aWriter), because all entries have to be collected first to be sorted before they can be written.Note that Guava has the subclasses
ImmutableSortedSetandImmutableSortedMap. Gson just serializes the entries in iteration order, so if the Set or Map has a well defined iteration order, then that is preserved.Side note: Unless you registered custom adapters (either your own, or from an external library) for
ImmutableSet/ImmutableMapyou might run into issues in Gson 2.13.0, see the release notes. Related: #2742The easiest way to implement this might be to write a
TypeAdapterFactorywhich delegates to your existing Map and Set adapters to create an in-memoryJsonElementtree, and afterwards sorts it before writing it.For example:
(click to expand / collapse)
And then register them like this:
Most likely you want to adjust the
Set.classandMap.classchecks in the#createmethods to be more specific and only handle the Set and Map implementations which really need sorting. Otherwise this decreases performance by redundantly sorting the JSON output for other Set and Map implementations.(Note: I have not extensively tested this, and maybe it can be refactored to improve performance. But it should work as desired.)
patkovskyi commentedon Apr 23, 2025
Thank you very much for a prompt and thoughtful reply. I'll study and test your workaround with TypeAdapterFactories.