From 920366e7845f79604a115c56525f000c3e3105b9 Mon Sep 17 00:00:00 2001 From: Marcono1234 Date: Wed, 26 Jul 2023 19:00:05 +0200 Subject: [PATCH 1/8] Throw exception when using classes from other JSON libraries with Gson --- Troubleshooting.md | 253 +++++++- gson/pom.xml | 8 + gson/src/main/java/com/google/gson/Gson.java | 4 + ...upportedJsonLibraryTypeAdapterFactory.java | 69 +++ .../gson/functional/JsonOrgInteropTest.java | 568 ++++++++++++++++++ 5 files changed, 901 insertions(+), 1 deletion(-) create mode 100644 gson/src/main/java/com/google/gson/internal/bind/UnsupportedJsonLibraryTypeAdapterFactory.java create mode 100644 gson/src/test/java/com/google/gson/functional/JsonOrgInteropTest.java diff --git a/Troubleshooting.md b/Troubleshooting.md index 184f19166e..29835e5b21 100644 --- a/Troubleshooting.md +++ b/Troubleshooting.md @@ -52,7 +52,258 @@ module mymodule { } ``` -Or in case this occurs for a field in one of your classes which you did not actually want to serialize or deserialize in the first place, you can exclude that field, see the [user guide](UserGuide.md#excluding-fields-from-serialization-and-deserialization). +## `RuntimeException`: 'Unsupported class from other JSON library: ...' + +**Symptom:** An exception with a message in the form 'Unsupported class from other JSON library: ...' is thrown + +**Reason:** You are using classes from a different JSON library with Gson, and because Gson does not support those classes it throws an exception to avoid unexpected serialization or deserialization results + +**Solution:** The easiest solution is to avoid mixing multiple JSON libraries; Gson provides the classes [`com.google.gson.JsonArray`](https://javadoc.io/doc/com.google.code.gson/gson/latest/com.google.gson/com/google/gson/JsonArray.html) and [`com.google.gson.JsonObject`](https://javadoc.io/doc/com.google.code.gson/gson/latest/com.google.gson/com/google/gson/JsonObject.html) which you can use instead of the classes from the other JSON library. + +If you cannot switch the classes you are using, see the library-specific solutions below: + +- `org.json.JSONArray`, `org.json.JSONObject` ([JSON-java](https://github.com/stleary/JSON-java), Android) +
+ + (Click to show) + + If you cannot switch to the Gson classes, but the structure of the JSON data does not have to remain the same, you can use the following Gson `TypeAdapterFactory` which you have to [register on a `GsonBuilder`](https://javadoc.io/doc/com.google.code.gson/gson/latest/com.google.gson/com/google/gson/GsonBuilder.html#registerTypeAdapterFactory(com.google.gson.TypeAdapterFactory)): + + + ```java + /** + * {@code TypeAdapterFactory} for {@link JSONArray} and {@link JSONObject}. + * + *

This factory is mainly intended for applications which cannot switch to + * Gson's own {@link JsonArray} and {@link JsonObject} classes. + */ + public class JsonOrgAdapterFactory implements TypeAdapterFactory { + private abstract static class JsonOrgAdapter extends TypeAdapter { + private final TypeAdapter jsonElementAdapter; + + public JsonOrgAdapter(TypeAdapter jsonElementAdapter) { + this.jsonElementAdapter = jsonElementAdapter; + } + + protected abstract T readJsonOrgValue(String json); + + @Override + public T read(JsonReader in) throws IOException { + if (in.peek() == JsonToken.NULL) { + in.nextNull(); + return null; + } + + // For correctness convert JSON data to string, then let JSON-java parse it; + // this is pretty inefficient, but makes sure it gets all the corner cases + // of JSON-java correct + // However, unlike JSONObject this will not prevent duplicate member names + JsonElement jsonElement = jsonElementAdapter.read(in); + String json = jsonElementAdapter.toJson(jsonElement); + return readJsonOrgValue(json); + } + + @Override + public void write(JsonWriter out, T value) throws IOException { + if (value == null) { + out.nullValue(); + return; + } + + // For correctness let JSON-java perform JSON conversion, then parse again and write + // with Gson; this is pretty inefficient, but makes sure it gets all the corner cases + // of JSON-java correct + String json = value.toString(); + JsonElement jsonElement = jsonElementAdapter.fromJson(json); + jsonElementAdapter.write(out, jsonElement); + } + } + + @Override + public TypeAdapter create(Gson gson, TypeToken type) { + Class rawType = type.getRawType(); + if (rawType != JSONArray.class && rawType != JSONObject.class) { + return null; + } + + TypeAdapter jsonElementAdapter = gson.getAdapter(JsonElement.class); + + TypeAdapter adapter; + if (rawType == JSONArray.class) { + adapter = new JsonOrgAdapter(jsonElementAdapter) { + @Override + protected JSONArray readJsonOrgValue(String json) { + return new JSONArray(json); + } + }; + } else { + adapter = new JsonOrgAdapter(jsonElementAdapter) { + @Override + protected JSONObject readJsonOrgValue(String json) { + return new JSONObject(json); + } + }; + } + + // Safe due to type check at beginning of method + @SuppressWarnings("unchecked") + TypeAdapter t = (TypeAdapter) adapter; + return t; + } + } + ``` + + Otherwise, if for backward compatibility you also have to preserve the existing JSON structure which was previously produced by Gson's reflection-based adapter, you can use the following factory: + + + ```java + /** + * Custom {@code TypeAdapterFactory} for {@link JSONArray} and {@link JSONObject}, + * which uses a format similar to what Gson's reflection-based adapter would have + * used. + * + *

This factory is mainly intended for applications which in the past by accident + * relied on Gson's reflection-based adapter for {@code JSONArray} and {@code JSONObject} + * and now have to keep this format for backward compatibility. + */ + public class JsonOrgBackwardCompatibleAdapterFactory implements TypeAdapterFactory { + private abstract static class JsonOrgBackwardCompatibleAdapter extends TypeAdapter { + /** Internal field name used by JSON-java for the respective JSON value class */ + private final String fieldName; + private final TypeAdapter wrappedTypeAdapter; + + public JsonOrgBackwardCompatibleAdapter(String fieldName, TypeAdapter wrappedTypeAdapter) { + this.fieldName = fieldName; + this.wrappedTypeAdapter = wrappedTypeAdapter; + } + + protected abstract T createJsonOrgValue(W wrapped); + + @Override + public T read(JsonReader in) throws IOException { + if (in.peek() == JsonToken.NULL) { + in.nextNull(); + return null; + } + + in.beginObject(); + String name = in.nextName(); + if (!name.equals(fieldName)) { + throw new IllegalArgumentException("Unexpected name '" + name + "', expected '" + fieldName + "' at " + in.getPath()); + } + T value = createJsonOrgValue(wrappedTypeAdapter.read(in)); + in.endObject(); + + return value; + } + + protected abstract W getWrapped(T value); + + @Override + public void write(JsonWriter out, T value) throws IOException { + if (value == null) { + out.nullValue(); + return; + } + + out.beginObject(); + out.name(fieldName); + wrappedTypeAdapter.write(out, getWrapped(value)); + out.endObject(); + } + } + + @Override + public TypeAdapter create(Gson gson, TypeToken type) { + Class rawType = type.getRawType(); + + // Note: This handling for JSONObject.NULL is not the same as the previous Gson reflection-based + // behavior which would have written `{}`, but this implementation here probably makes more sense + if (rawType == JSONObject.NULL.getClass()) { + return new TypeAdapter() { + @Override + public T read(JsonReader in) throws IOException { + in.nextNull(); + return null; + } + + @Override + public void write(JsonWriter out, T value) throws IOException { + out.nullValue(); + } + }; + } + + if (rawType != JSONArray.class && rawType != JSONObject.class) { + return null; + } + + TypeAdapter adapter; + if (rawType == JSONArray.class) { + TypeAdapter> wrappedAdapter = gson.getAdapter(new TypeToken> () {}); + adapter = new JsonOrgBackwardCompatibleAdapter, JSONArray>("myArrayList", wrappedAdapter) { + @Override + protected JSONArray createJsonOrgValue(List wrapped) { + JSONArray jsonArray = new JSONArray(wrapped.size()); + // Unlike JSONArray(Collection) constructor, putAll does not wrap elements and is therefore closer + // to original Gson reflection-based behavior + jsonArray.putAll(wrapped); + + return jsonArray; + } + + @Override + protected List getWrapped(JSONArray jsonArray) { + // Cannot use JSONArray.toList() because that converts elements + List list = new ArrayList<>(jsonArray.length()); + for (Object element : jsonArray) { + list.add(element); + } + + return list; + } + }; + } else { + TypeAdapter> wrappedAdapter = gson.getAdapter(new TypeToken> () {}); + adapter = new JsonOrgBackwardCompatibleAdapter, JSONObject>("map", wrappedAdapter) { + @Override + protected JSONObject createJsonOrgValue(Map map) { + // JSONObject(Map) constructor wraps elements, so instead put elements separately to be closer + // to original Gson reflection-based behavior + JSONObject jsonObject = new JSONObject(); + for (Entry entry : map.entrySet()) { + jsonObject.put(entry.getKey(), entry.getValue()); + } + + return jsonObject; + } + + @Override + protected Map getWrapped(JSONObject jsonObject) { + // Cannot use JSONObject.toMap() because that converts elements + Map map = new LinkedHashMap<>(jsonObject.length()); + for (String name : jsonObject.keySet()) { + // Use opt(String) because get(String) cannot handle null values + // Most likely null values cannot occur normally though; they would be JSONObject.NULL + map.put(name, jsonObject.opt(name)); + } + + return map; + } + }; + } + + // Safe due to type check at beginning of method + @SuppressWarnings("unchecked") + TypeAdapter t = (TypeAdapter) adapter; + return t; + } + } + ``` + + **Important:** Verify carefully that these `TypeAdapterFactory` classes work as expected for your use case and produce the desired JSON data or parse the JSON data without issues. There might be corner cases where they behave slightly differently than Gson's reflection-based adapter, respectively behave differently than the other JSON library would behave. + + ## Android app not working in Release mode; random property names diff --git a/gson/pom.xml b/gson/pom.xml index 9a48ef4ded..4e4708c0d9 100644 --- a/gson/pom.xml +++ b/gson/pom.xml @@ -49,6 +49,14 @@ 2.20.0 + + + org.json + json + 20230618 + test + + junit junit diff --git a/gson/src/main/java/com/google/gson/Gson.java b/gson/src/main/java/com/google/gson/Gson.java index c6f8508ef1..2d6daa3fa5 100644 --- a/gson/src/main/java/com/google/gson/Gson.java +++ b/gson/src/main/java/com/google/gson/Gson.java @@ -25,6 +25,7 @@ import com.google.gson.internal.bind.ArrayTypeAdapter; import com.google.gson.internal.bind.CollectionTypeAdapterFactory; import com.google.gson.internal.bind.DateTypeAdapter; +import com.google.gson.internal.bind.UnsupportedJsonLibraryTypeAdapterFactory; import com.google.gson.internal.bind.JsonAdapterAnnotationTypeAdapterFactory; import com.google.gson.internal.bind.JsonTreeReader; import com.google.gson.internal.bind.JsonTreeWriter; @@ -340,6 +341,9 @@ public Gson() { this.jsonAdapterFactory = new JsonAdapterAnnotationTypeAdapterFactory(constructorConstructor); factories.add(jsonAdapterFactory); factories.add(TypeAdapters.ENUM_FACTORY); + // Register this right before reflection-based adapter to allow other adapters to handle these + // types (if possible) and to let users specify their own custom adapters + factories.add(UnsupportedJsonLibraryTypeAdapterFactory.INSTANCE); factories.add(new ReflectiveTypeAdapterFactory( constructorConstructor, fieldNamingStrategy, excluder, jsonAdapterFactory, reflectionFilters)); diff --git a/gson/src/main/java/com/google/gson/internal/bind/UnsupportedJsonLibraryTypeAdapterFactory.java b/gson/src/main/java/com/google/gson/internal/bind/UnsupportedJsonLibraryTypeAdapterFactory.java new file mode 100644 index 0000000000..d6a1382c17 --- /dev/null +++ b/gson/src/main/java/com/google/gson/internal/bind/UnsupportedJsonLibraryTypeAdapterFactory.java @@ -0,0 +1,69 @@ +package com.google.gson.internal.bind; + +import com.google.gson.Gson; +import com.google.gson.TypeAdapter; +import com.google.gson.TypeAdapterFactory; +import com.google.gson.internal.TroubleshootingGuide; +import com.google.gson.reflect.TypeToken; +import com.google.gson.stream.JsonReader; +import com.google.gson.stream.JsonWriter; +import java.io.IOException; +import java.util.Arrays; +import java.util.HashSet; +import java.util.Set; + +/** + * {@code TypeAdapterFactory} which throws an exception when trying to serialize or + * deserialize unsupported classes from third-party JSON libraries. + * + *

This is mainly intended as help for users who accidentally mix Gson and non-Gson + * code and are then surprised by unexpected JSON data or issues when trying to + * deserialize the JSON data. + */ +public class UnsupportedJsonLibraryTypeAdapterFactory implements TypeAdapterFactory { + public static final UnsupportedJsonLibraryTypeAdapterFactory INSTANCE = new UnsupportedJsonLibraryTypeAdapterFactory(); + + private UnsupportedJsonLibraryTypeAdapterFactory() { + } + + // Cover JSON classes from popular libraries which might be used by accident with Gson + // Don't have to cover classes which implement `Collection` / `List` or `Map` because + // Gson's built-in adapters for these types should be able to handle them just fine + private static final Set UNSUPPORTED_CLASS_NAMES = new HashSet<>(Arrays.asList( + // https://github.com/stleary/JSON-java and Android + "org.json.JSONArray", + "org.json.JSONObject", + // https://github.com/eclipse-vertx/vert.x + "io.vertx.core.json.JsonArray", + "io.vertx.core.json.JsonObject" + )); + + @Override + public TypeAdapter create(Gson gson, TypeToken type) { + final String className = type.getRawType().getName(); + if (!UNSUPPORTED_CLASS_NAMES.contains(className)) { + return null; + } + + // Don't directly throw exception here in case no instance of the class is every serialized + // or deserialized, instead only thrown when actual serialization or deserialization attempt + // occurs + return new TypeAdapter() { + private RuntimeException createException() { + // TODO: Use more specific exception type; also adjust Troubleshooting.md entry then + return new RuntimeException("Unsupported class from other JSON library: " + className + + "\nSee " + TroubleshootingGuide.createUrl("unsupported-json-library-class")); + } + + @Override + public T read(JsonReader in) throws IOException { + throw createException(); + } + + @Override + public void write(JsonWriter out, T value) throws IOException { + throw createException(); + } + }; + } +} diff --git a/gson/src/test/java/com/google/gson/functional/JsonOrgInteropTest.java b/gson/src/test/java/com/google/gson/functional/JsonOrgInteropTest.java new file mode 100644 index 0000000000..bc0cc943a8 --- /dev/null +++ b/gson/src/test/java/com/google/gson/functional/JsonOrgInteropTest.java @@ -0,0 +1,568 @@ +package com.google.gson.functional; + +import static com.google.common.truth.Truth.assertAbout; +import static com.google.common.truth.Truth.assertThat; +import static org.junit.Assert.assertThrows; + +import com.google.common.truth.FailureMetadata; +import com.google.common.truth.StandardSubjectBuilder; +import com.google.common.truth.Subject; +import com.google.gson.Gson; +import com.google.gson.GsonBuilder; +import com.google.gson.JsonArray; +import com.google.gson.JsonElement; +import com.google.gson.JsonObject; +import com.google.gson.TypeAdapter; +import com.google.gson.TypeAdapterFactory; +import com.google.gson.reflect.TypeToken; +import com.google.gson.stream.JsonReader; +import com.google.gson.stream.JsonToken; +import com.google.gson.stream.JsonWriter; +import java.io.IOException; +import java.math.BigDecimal; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.Collections; +import java.util.HashMap; +import java.util.LinkedHashMap; +import java.util.List; +import java.util.Map; +import java.util.Map.Entry; +import org.checkerframework.checker.nullness.qual.Nullable; +import org.json.JSONArray; +import org.json.JSONObject; +import org.json.JSONString; +import org.junit.Test; + +/** + * Tests interoperability with https://github.com/stleary/JSON-java ({@code org.json} package). + */ +public class JsonOrgInteropTest { + @Test + public void testNoCustomAdapter() { + Gson gson = new Gson(); + String expectedMessageArray = "Unsupported class from other JSON library: org.json.JSONArray" + + "\nSee https://github.com/google/gson/blob/main/Troubleshooting.md#unsupported-json-library-class"; + String expectedMessageObject = "Unsupported class from other JSON library: org.json.JSONObject" + + "\nSee https://github.com/google/gson/blob/main/Troubleshooting.md#unsupported-json-library-class"; + + // TODO: Adjust these once more specific exception type than RuntimeException is thrown + Exception e = assertThrows(RuntimeException.class, () -> gson.toJson(new JSONArray())); + assertThat(e).hasMessageThat().isEqualTo(expectedMessageArray); + + e = assertThrows(RuntimeException.class, () -> gson.toJson(new JSONObject())); + assertThat(e).hasMessageThat().isEqualTo(expectedMessageObject); + + e = assertThrows(RuntimeException.class, () -> gson.fromJson("[]", JSONArray.class)); + assertThat(e).hasMessageThat().isEqualTo(expectedMessageArray); + + e = assertThrows(RuntimeException.class, () -> gson.fromJson("{}", JSONObject.class)); + assertThat(e).hasMessageThat().isEqualTo(expectedMessageObject); + } + + // Custom classes for equality assertions to avoid using directly JSONArray and JSONObject + // which perform element wrapping and conversion, and because their `toList()` and `toMap()` + // methods also recursively convert values + private static class ExpectedJSONArray { + public final List elements; + + public ExpectedJSONArray(List elements) { + this.elements = elements; + } + + @Override + public String toString() { + return "JSONArray" + elements; + } + } + + private static class ExpectedJSONObject { + public final Map entries; + + public ExpectedJSONObject(Map entries) { + this.entries = entries; + } + + @Override + public String toString() { + return "JSONObject" + entries; + } + } + + private abstract static class JsonOrgBaseSubject extends Subject { + + protected JsonOrgBaseSubject(FailureMetadata metadata, @Nullable Object actual) { + super(metadata, actual); + } + + protected void checkElementValues(String message, Object expected, Object actual) { + StandardSubjectBuilder builder = check(message); + + if (actual instanceof JSONArray) { + builder.about(JSONArraySubject.jsonArrays()).that((JSONArray) actual).isEqualTo(expected); + } else if (actual instanceof JSONObject) { + builder.about(JSONObjectSubject.jsonObjects()).that((JSONObject) actual).isEqualTo(expected); + } else { + builder.that(actual).isEqualTo(expected); + } + } + } + + private static class JSONArraySubject extends JsonOrgBaseSubject { + private final @Nullable JSONArray actual; + + private JSONArraySubject(FailureMetadata failureMetadata, @Nullable JSONArray subject) { + super(failureMetadata, subject); + this.actual = subject; + } + + public static Factory jsonArrays() { + return JSONArraySubject::new; + } + + public static JSONArraySubject assertThat(@Nullable JSONArray actual) { + return assertAbout(JSONArraySubject.jsonArrays()).that(actual); + } + + @Override + public void isEqualTo(Object expected) { + if (!(expected instanceof ExpectedJSONArray)) { + failWithActual("did not expect to be", "a JSONArray"); + } + isNotNull(); + + List expectedElements = ((ExpectedJSONArray) expected).elements; + check("length()").that(actual.length()).isEqualTo(expectedElements.size()); + + for (int i = 0; i < expectedElements.size(); i++) { + Object actualElement = actual.opt(i); + Object expectedElement = expectedElements.get(i); + + checkElementValues("elements[" + i + "]", expectedElement, actualElement); + } + } + } + + private static class JSONObjectSubject extends JsonOrgBaseSubject { + private final @Nullable JSONObject actual; + + private JSONObjectSubject(FailureMetadata failureMetadata, @Nullable JSONObject subject) { + super(failureMetadata, subject); + this.actual = subject; + } + + public static Factory jsonObjects() { + return JSONObjectSubject::new; + } + + public static JSONObjectSubject assertThat(@Nullable JSONObject actual) { + return assertAbout(JSONObjectSubject.jsonObjects()).that(actual); + } + + @Override + public void isEqualTo(Object expected) { + if (!(expected instanceof ExpectedJSONObject)) { + failWithActual("did not expect to be", "a JSONObject"); + } + isNotNull(); + + Map expectedEntries = ((ExpectedJSONObject) expected).entries; + check("length()").that(actual.length()).isEqualTo(expectedEntries.size()); + + for (Entry expectedEntry : expectedEntries.entrySet()) { + String expectedKey = expectedEntry.getKey(); + Object actualValue = actual.opt(expectedKey); + + checkElementValues("entries[" + expectedKey + "]", expectedEntry.getValue(), actualValue); + } + } + } + + + + private static class CustomClass { + @SuppressWarnings("unused") + int i = 1; + + @Override + public String toString() { + return "custom-toString"; + } + } + + private static class CustomJsonStringClass implements JSONString { + @Override + public String toJSONString() { + return "\"custom\""; + } + } + + // Important: Make sure this class is in-sync with the code in Troubleshooting.md + /** + * {@code TypeAdapterFactory} for {@link JSONArray} and {@link JSONObject}. + * + *

This factory is mainly intended for applications which cannot switch to + * Gson's own {@link JsonArray} and {@link JsonObject} classes. + */ + private static class JsonOrgAdapterFactory implements TypeAdapterFactory { + private abstract static class JsonOrgAdapter extends TypeAdapter { + private final TypeAdapter jsonElementAdapter; + + public JsonOrgAdapter(TypeAdapter jsonElementAdapter) { + this.jsonElementAdapter = jsonElementAdapter; + } + + protected abstract T readJsonOrgValue(String json); + + @Override + public T read(JsonReader in) throws IOException { + if (in.peek() == JsonToken.NULL) { + in.nextNull(); + return null; + } + + // For correctness convert JSON data to string, then let JSON-java parse it; + // this is pretty inefficient, but makes sure it gets all the corner cases + // of JSON-java correct + // However, unlike JSONObject this will not prevent duplicate member names + JsonElement jsonElement = jsonElementAdapter.read(in); + String json = jsonElementAdapter.toJson(jsonElement); + return readJsonOrgValue(json); + } + + @Override + public void write(JsonWriter out, T value) throws IOException { + if (value == null) { + out.nullValue(); + return; + } + + // For correctness let JSON-java perform JSON conversion, then parse again and write + // with Gson; this is pretty inefficient, but makes sure it gets all the corner cases + // of JSON-java correct + String json = value.toString(); + JsonElement jsonElement = jsonElementAdapter.fromJson(json); + jsonElementAdapter.write(out, jsonElement); + } + } + + @Override + public TypeAdapter create(Gson gson, TypeToken type) { + Class rawType = type.getRawType(); + if (rawType != JSONArray.class && rawType != JSONObject.class) { + return null; + } + + TypeAdapter jsonElementAdapter = gson.getAdapter(JsonElement.class); + + TypeAdapter adapter; + if (rawType == JSONArray.class) { + adapter = new JsonOrgAdapter(jsonElementAdapter) { + @Override + protected JSONArray readJsonOrgValue(String json) { + return new JSONArray(json); + } + }; + } else { + adapter = new JsonOrgAdapter(jsonElementAdapter) { + @Override + protected JSONObject readJsonOrgValue(String json) { + return new JSONObject(json); + } + }; + } + + // Safe due to type check at beginning of method + @SuppressWarnings("unchecked") + TypeAdapter t = (TypeAdapter) adapter; + return t; + } + } + + /** + * Tests usage of custom adapters for {@link JSONArray} and {@link JSONObject}. + * + *

This test also verifies that the code shown in {@code Troubleshooting.md} works + * as expected. + */ + @Test + public void testCustomAdapters() { + Gson gson = new GsonBuilder() + .serializeNulls() + .registerTypeAdapterFactory(new JsonOrgAdapterFactory()) + .create(); + + JSONArray array = new JSONArray(Arrays.asList( + null, + JSONObject.NULL, + new CustomClass(), + new CustomJsonStringClass(), + new BigDecimal("123.4"), + true, + new JSONObject(Collections.singletonMap("key", 1)), + new JSONArray(Arrays.asList(2)), + Collections.singletonMap("key", 3), + Arrays.asList(4), + new boolean[] {false} + )); + assertThat(gson.toJson(array)).isEqualTo( + "[null,null,{},\"custom\",123.4,true,{\"key\":1},[2],{\"key\":3},[4],[false]]"); + assertThat(gson.toJson(null, JSONArray.class)).isEqualTo("null"); + + JSONObject object = new JSONObject(); + object.put("1", JSONObject.NULL); + object.put("2", new CustomClass()); + object.put("3", new CustomJsonStringClass()); + object.put("4", new BigDecimal("123.4")); + object.put("5", true); + object.put("6", new JSONObject(Collections.singletonMap("key", 1))); + object.put("7", new JSONArray(Arrays.asList(2))); + object.put("8", Collections.singletonMap("key", 3)); + object.put("9", Arrays.asList(4)); + object.put("10", new boolean[] {false}); + assertThat(gson.toJson(object)).isEqualTo( + "{\"1\":null,\"2\":\"custom-toString\",\"3\":\"custom\",\"4\":123.4,\"5\":true,\"6\":{\"key\":1},\"7\":[2],\"8\":{\"key\":3},\"9\":[4],\"10\":[false]}"); + assertThat(gson.toJson(null, JSONObject.class)).isEqualTo("null"); + + ExpectedJSONArray expectedArray = new ExpectedJSONArray(Arrays.asList( + JSONObject.NULL, + true, + 12, + "string", + new ExpectedJSONObject(Collections.singletonMap("key", 1)), + new ExpectedJSONArray(Arrays.asList(2)) + )); + String json = "[null, true, 12, \"string\", {\"key\": 1}, [2]]"; + JSONArraySubject.assertThat(gson.fromJson(json, JSONArray.class)).isEqualTo(expectedArray); + assertThat(gson.fromJson("null", JSONArray.class)).isNull(); + + Map expectedObject = new HashMap<>(); + expectedObject.put("1", JSONObject.NULL); + expectedObject.put("2", true); + expectedObject.put("3", 12); + expectedObject.put("4", "string"); + expectedObject.put("5", new ExpectedJSONObject(Collections.singletonMap("key", 1))); + expectedObject.put("6", new ExpectedJSONArray(Arrays.asList(2))); + json = "{\"1\": null, \"2\": true, \"3\": 12, \"4\": \"string\", \"5\": {\"key\": 1}, \"6\": [2]}"; + JSONObjectSubject.assertThat(gson.fromJson(json, JSONObject.class)).isEqualTo(new ExpectedJSONObject(expectedObject)); + assertThat(gson.fromJson("null", JSONObject.class)).isNull(); + } + + // Important: Make sure this class is in-sync with the code in Troubleshooting.md + /** + * Custom {@code TypeAdapterFactory} for {@link JSONArray} and {@link JSONObject}, + * which uses a format similar to what Gson's reflection-based adapter would have + * used. + * + *

This factory is mainly intended for applications which in the past by accident + * relied on Gson's reflection-based adapter for {@code JSONArray} and {@code JSONObject} + * and now have to keep this format for backward compatibility. + */ + private static class JsonOrgBackwardCompatibleAdapterFactory implements TypeAdapterFactory { + private abstract static class JsonOrgBackwardCompatibleAdapter extends TypeAdapter { + /** Internal field name used by JSON-java for the respective JSON value class */ + private final String fieldName; + private final TypeAdapter wrappedTypeAdapter; + + public JsonOrgBackwardCompatibleAdapter(String fieldName, TypeAdapter wrappedTypeAdapter) { + this.fieldName = fieldName; + this.wrappedTypeAdapter = wrappedTypeAdapter; + } + + protected abstract T createJsonOrgValue(W wrapped); + + @Override + public T read(JsonReader in) throws IOException { + if (in.peek() == JsonToken.NULL) { + in.nextNull(); + return null; + } + + in.beginObject(); + String name = in.nextName(); + if (!name.equals(fieldName)) { + throw new IllegalArgumentException("Unexpected name '" + name + "', expected '" + fieldName + "' at " + in.getPath()); + } + T value = createJsonOrgValue(wrappedTypeAdapter.read(in)); + in.endObject(); + + return value; + } + + protected abstract W getWrapped(T value); + + @Override + public void write(JsonWriter out, T value) throws IOException { + if (value == null) { + out.nullValue(); + return; + } + + out.beginObject(); + out.name(fieldName); + wrappedTypeAdapter.write(out, getWrapped(value)); + out.endObject(); + } + } + + @Override + public TypeAdapter create(Gson gson, TypeToken type) { + Class rawType = type.getRawType(); + + // Note: This handling for JSONObject.NULL is not the same as the previous Gson reflection-based + // behavior which would have written `{}`, but this implementation here probably makes more sense + if (rawType == JSONObject.NULL.getClass()) { + return new TypeAdapter() { + @Override + public T read(JsonReader in) throws IOException { + in.nextNull(); + return null; + } + + @Override + public void write(JsonWriter out, T value) throws IOException { + out.nullValue(); + } + }; + } + + if (rawType != JSONArray.class && rawType != JSONObject.class) { + return null; + } + + TypeAdapter adapter; + if (rawType == JSONArray.class) { + TypeAdapter> wrappedAdapter = gson.getAdapter(new TypeToken> () {}); + adapter = new JsonOrgBackwardCompatibleAdapter, JSONArray>("myArrayList", wrappedAdapter) { + @Override + protected JSONArray createJsonOrgValue(List wrapped) { + JSONArray jsonArray = new JSONArray(wrapped.size()); + // Unlike JSONArray(Collection) constructor, putAll does not wrap elements and is therefore closer + // to original Gson reflection-based behavior + jsonArray.putAll(wrapped); + + return jsonArray; + } + + @Override + protected List getWrapped(JSONArray jsonArray) { + // Cannot use JSONArray.toList() because that converts elements + List list = new ArrayList<>(jsonArray.length()); + for (Object element : jsonArray) { + list.add(element); + } + + return list; + } + }; + } else { + TypeAdapter> wrappedAdapter = gson.getAdapter(new TypeToken> () {}); + adapter = new JsonOrgBackwardCompatibleAdapter, JSONObject>("map", wrappedAdapter) { + @Override + protected JSONObject createJsonOrgValue(Map map) { + // JSONObject(Map) constructor wraps elements, so instead put elements separately to be closer + // to original Gson reflection-based behavior + JSONObject jsonObject = new JSONObject(); + for (Entry entry : map.entrySet()) { + jsonObject.put(entry.getKey(), entry.getValue()); + } + + return jsonObject; + } + + @Override + protected Map getWrapped(JSONObject jsonObject) { + // Cannot use JSONObject.toMap() because that converts elements + Map map = new LinkedHashMap<>(jsonObject.length()); + for (String name : jsonObject.keySet()) { + // Use opt(String) because get(String) cannot handle null values + // Most likely null values cannot occur normally though; they would be JSONObject.NULL + map.put(name, jsonObject.opt(name)); + } + + return map; + } + }; + } + + // Safe due to type check at beginning of method + @SuppressWarnings("unchecked") + TypeAdapter t = (TypeAdapter) adapter; + return t; + } + } + + /** + * Tests usage of custom adapters for {@link JSONArray} and {@link JSONObject}, + * which serialize and deserialize these classes in (nearly) the same format which the + * reflection-based adapter would use for them. + * + *

This test also verifies that the code shown in {@code Troubleshooting.md} works + * as expected. + */ + @Test + public void testCustomBackwardCompatibleAdapters() { + Gson gson = new GsonBuilder() + .serializeNulls() + .registerTypeAdapterFactory(new JsonOrgBackwardCompatibleAdapterFactory()) + .create(); + + JSONArray array = new JSONArray(Arrays.asList( + null, + JSONObject.NULL, + new BigDecimal("123.4"), + true, + new JSONObject(Collections.singletonMap("key", 1)), + new JSONArray(Arrays.asList(2)), + Collections.singletonMap("key", 3), + Arrays.asList(4), + new boolean[] {false} + )); + assertThat(gson.toJson(array)).isEqualTo( + "{\"myArrayList\":[null,null,123.4,true,{\"map\":{\"key\":1}},{\"myArrayList\":[2]},{\"map\":{\"key\":3}},{\"myArrayList\":[4]},{\"myArrayList\":[false]}]}"); + assertThat(gson.toJson(null, JSONArray.class)).isEqualTo("null"); + + JSONObject object = new JSONObject(); + object.put("1", JSONObject.NULL); + object.put("2", new BigDecimal("123.4")); + object.put("3", true); + object.put("4", new JSONObject(Collections.singletonMap("key", 1))); + object.put("5", new JSONArray(Arrays.asList(2))); + object.put("6", Collections.singletonMap("key", 3)); + object.put("7", Arrays.asList(4)); + object.put("8", new boolean[] {false}); + assertThat(gson.toJson(object)).isEqualTo( + "{\"map\":{\"1\":null,\"2\":123.4,\"3\":true,\"4\":{\"map\":{\"key\":1}},\"5\":{\"myArrayList\":[2]},\"6\":{\"map\":{\"key\":3}},\"7\":{\"myArrayList\":[4]},\"8\":[false]}}"); + assertThat(gson.toJson(null, JSONObject.class)).isEqualTo("null"); + + ExpectedJSONArray expectedArray = new ExpectedJSONArray(Arrays.asList( + null, + true, + 12.0, + "string", + Collections.singletonMap("key", 1.0), + // Nested JSONObject cannot be restored properly + Collections.singletonMap("map", Collections.singletonMap("key", 2.0)), + Arrays.asList(3.0), + // Nested JSONArray cannot be restored properly + Collections.singletonMap("myArrayList", Arrays.asList(4.0)) + )); + String json = "{\"myArrayList\": [null, true, 12, \"string\", {\"key\": 1}, {\"map\": {\"key\": 2}}, [3], {\"myArrayList\": [4]}]}"; + JSONArraySubject.assertThat(gson.fromJson(json, JSONArray.class)).isEqualTo(expectedArray); + assertThat(gson.fromJson("null", JSONArray.class)).isNull(); + + Map expectedObject = new HashMap<>(); + expectedObject.put("1", true); + expectedObject.put("2", 12.0); + expectedObject.put("3", "string"); + expectedObject.put("4", Collections.singletonMap("key", 1.0)); + // Nested JSONObject cannot be restored properly + expectedObject.put("5", Collections.singletonMap("map", Collections.singletonMap("key", 2.0))); + expectedObject.put("6", Arrays.asList(3.0)); + // Nested JSONArray cannot be restored properly + expectedObject.put("7", Collections.singletonMap("myArrayList", Arrays.asList(4.0))); + json = "{\"map\": {\"1\": true, \"2\": 12, \"3\": \"string\", \"4\": {\"key\": 1}, \"5\": {\"map\": {\"key\": 2}}, \"6\": [3], \"7\": {\"myArrayList\": [4]}}}"; + JSONObjectSubject.assertThat(gson.fromJson(json, JSONObject.class)).isEqualTo(new ExpectedJSONObject(expectedObject)); + assertThat(gson.fromJson("null", JSONObject.class)).isNull(); + } +} From ac57016d67b5ca4d524b06898fa944758a231b25 Mon Sep 17 00:00:00 2001 From: Marcono1234 Date: Wed, 26 Jul 2023 19:39:45 +0200 Subject: [PATCH 2/8] Switch to old JSON-java version to match Android API --- Troubleshooting.md | 46 +++++++++---- gson/pom.xml | 4 +- .../gson/functional/JsonOrgInteropTest.java | 65 ++++++++++++------- 3 files changed, 79 insertions(+), 36 deletions(-) diff --git a/Troubleshooting.md b/Troubleshooting.md index 29835e5b21..3bc40135f8 100644 --- a/Troubleshooting.md +++ b/Troubleshooting.md @@ -85,7 +85,7 @@ If you cannot switch the classes you are using, see the library-specific solutio this.jsonElementAdapter = jsonElementAdapter; } - protected abstract T readJsonOrgValue(String json); + protected abstract T readJsonOrgValue(String json) throws JSONException; @Override public T read(JsonReader in) throws IOException { @@ -100,7 +100,13 @@ If you cannot switch the classes you are using, see the library-specific solutio // However, unlike JSONObject this will not prevent duplicate member names JsonElement jsonElement = jsonElementAdapter.read(in); String json = jsonElementAdapter.toJson(jsonElement); - return readJsonOrgValue(json); + try { + return readJsonOrgValue(json); + } + // For Android this is a checked exception; for the latest JSON-java artifacts it isn't anymore + catch (JSONException e) { + throw new RuntimeException(e); + } } @Override @@ -132,14 +138,14 @@ If you cannot switch the classes you are using, see the library-specific solutio if (rawType == JSONArray.class) { adapter = new JsonOrgAdapter(jsonElementAdapter) { @Override - protected JSONArray readJsonOrgValue(String json) { + protected JSONArray readJsonOrgValue(String json) throws JSONException { return new JSONArray(json); } }; } else { adapter = new JsonOrgAdapter(jsonElementAdapter) { @Override - protected JSONObject readJsonOrgValue(String json) { + protected JSONObject readJsonOrgValue(String json) throws JSONException { return new JSONObject(json); } }; @@ -177,7 +183,7 @@ If you cannot switch the classes you are using, see the library-specific solutio this.wrappedTypeAdapter = wrappedTypeAdapter; } - protected abstract T createJsonOrgValue(W wrapped); + protected abstract T createJsonOrgValue(W wrapped) throws JSONException; @Override public T read(JsonReader in) throws IOException { @@ -191,7 +197,14 @@ If you cannot switch the classes you are using, see the library-specific solutio if (!name.equals(fieldName)) { throw new IllegalArgumentException("Unexpected name '" + name + "', expected '" + fieldName + "' at " + in.getPath()); } - T value = createJsonOrgValue(wrappedTypeAdapter.read(in)); + T value; + try { + value = createJsonOrgValue(wrappedTypeAdapter.read(in)); + } + // For Android this is a checked exception; for the latest JSON-java artifacts it isn't anymore + catch (JSONException e) { + throw new RuntimeException(e); + } in.endObject(); return value; @@ -243,11 +256,13 @@ If you cannot switch the classes you are using, see the library-specific solutio TypeAdapter> wrappedAdapter = gson.getAdapter(new TypeToken> () {}); adapter = new JsonOrgBackwardCompatibleAdapter, JSONArray>("myArrayList", wrappedAdapter) { @Override - protected JSONArray createJsonOrgValue(List wrapped) { - JSONArray jsonArray = new JSONArray(wrapped.size()); - // Unlike JSONArray(Collection) constructor, putAll does not wrap elements and is therefore closer + protected JSONArray createJsonOrgValue(List wrapped) throws JSONException { + JSONArray jsonArray = new JSONArray(); + // Unlike JSONArray(Collection) constructor, `put` does not wrap elements and is therefore closer // to original Gson reflection-based behavior - jsonArray.putAll(wrapped); + for (Object element : wrapped) { + jsonArray.put(element); + } return jsonArray; } @@ -256,7 +271,9 @@ If you cannot switch the classes you are using, see the library-specific solutio protected List getWrapped(JSONArray jsonArray) { // Cannot use JSONArray.toList() because that converts elements List list = new ArrayList<>(jsonArray.length()); - for (Object element : jsonArray) { + for (int i = 0; i < jsonArray.length(); i++) { + // Use opt(int) because get(int) cannot handle null values + Object element = jsonArray.opt(i); list.add(element); } @@ -267,7 +284,7 @@ If you cannot switch the classes you are using, see the library-specific solutio TypeAdapter> wrappedAdapter = gson.getAdapter(new TypeToken> () {}); adapter = new JsonOrgBackwardCompatibleAdapter, JSONObject>("map", wrappedAdapter) { @Override - protected JSONObject createJsonOrgValue(Map map) { + protected JSONObject createJsonOrgValue(Map map) throws JSONException { // JSONObject(Map) constructor wraps elements, so instead put elements separately to be closer // to original Gson reflection-based behavior JSONObject jsonObject = new JSONObject(); @@ -282,7 +299,10 @@ If you cannot switch the classes you are using, see the library-specific solutio protected Map getWrapped(JSONObject jsonObject) { // Cannot use JSONObject.toMap() because that converts elements Map map = new LinkedHashMap<>(jsonObject.length()); - for (String name : jsonObject.keySet()) { + @SuppressWarnings("unchecked") // Old JSON-java versions return just `Iterator` instead of `Iterator` + Iterator names = jsonObject.keys(); + while (names.hasNext()) { + String name = names.next(); // Use opt(String) because get(String) cannot handle null values // Most likely null values cannot occur normally though; they would be JSONObject.NULL map.put(name, jsonObject.opt(name)); diff --git a/gson/pom.xml b/gson/pom.xml index 4e4708c0d9..cd21541c19 100644 --- a/gson/pom.xml +++ b/gson/pom.xml @@ -53,7 +53,9 @@ org.json json - 20230618 + + 20090211 test diff --git a/gson/src/test/java/com/google/gson/functional/JsonOrgInteropTest.java b/gson/src/test/java/com/google/gson/functional/JsonOrgInteropTest.java index bc0cc943a8..567408f5ea 100644 --- a/gson/src/test/java/com/google/gson/functional/JsonOrgInteropTest.java +++ b/gson/src/test/java/com/google/gson/functional/JsonOrgInteropTest.java @@ -19,17 +19,18 @@ import com.google.gson.stream.JsonToken; import com.google.gson.stream.JsonWriter; import java.io.IOException; -import java.math.BigDecimal; import java.util.ArrayList; import java.util.Arrays; import java.util.Collections; import java.util.HashMap; +import java.util.Iterator; import java.util.LinkedHashMap; import java.util.List; import java.util.Map; import java.util.Map.Entry; import org.checkerframework.checker.nullness.qual.Nullable; import org.json.JSONArray; +import org.json.JSONException; import org.json.JSONObject; import org.json.JSONString; import org.junit.Test; @@ -212,7 +213,7 @@ public JsonOrgAdapter(TypeAdapter jsonElementAdapter) { this.jsonElementAdapter = jsonElementAdapter; } - protected abstract T readJsonOrgValue(String json); + protected abstract T readJsonOrgValue(String json) throws JSONException; @Override public T read(JsonReader in) throws IOException { @@ -227,7 +228,13 @@ public T read(JsonReader in) throws IOException { // However, unlike JSONObject this will not prevent duplicate member names JsonElement jsonElement = jsonElementAdapter.read(in); String json = jsonElementAdapter.toJson(jsonElement); - return readJsonOrgValue(json); + try { + return readJsonOrgValue(json); + } + // For Android this is a checked exception; for the latest JSON-java artifacts it isn't anymore + catch (JSONException e) { + throw new RuntimeException(e); + } } @Override @@ -259,14 +266,14 @@ public TypeAdapter create(Gson gson, TypeToken type) { if (rawType == JSONArray.class) { adapter = new JsonOrgAdapter(jsonElementAdapter) { @Override - protected JSONArray readJsonOrgValue(String json) { + protected JSONArray readJsonOrgValue(String json) throws JSONException { return new JSONArray(json); } }; } else { adapter = new JsonOrgAdapter(jsonElementAdapter) { @Override - protected JSONObject readJsonOrgValue(String json) { + protected JSONObject readJsonOrgValue(String json) throws JSONException { return new JSONObject(json); } }; @@ -286,7 +293,7 @@ protected JSONObject readJsonOrgValue(String json) { * as expected. */ @Test - public void testCustomAdapters() { + public void testCustomAdapters() throws JSONException { Gson gson = new GsonBuilder() .serializeNulls() .registerTypeAdapterFactory(new JsonOrgAdapterFactory()) @@ -297,7 +304,7 @@ public void testCustomAdapters() { JSONObject.NULL, new CustomClass(), new CustomJsonStringClass(), - new BigDecimal("123.4"), + 123.4, true, new JSONObject(Collections.singletonMap("key", 1)), new JSONArray(Arrays.asList(2)), @@ -306,14 +313,14 @@ public void testCustomAdapters() { new boolean[] {false} )); assertThat(gson.toJson(array)).isEqualTo( - "[null,null,{},\"custom\",123.4,true,{\"key\":1},[2],{\"key\":3},[4],[false]]"); + "[null,null,\"custom-toString\",\"custom\",123.4,true,{\"key\":1},[2],{\"key\":3},[4],[false]]"); assertThat(gson.toJson(null, JSONArray.class)).isEqualTo("null"); JSONObject object = new JSONObject(); object.put("1", JSONObject.NULL); object.put("2", new CustomClass()); object.put("3", new CustomJsonStringClass()); - object.put("4", new BigDecimal("123.4")); + object.put("4", 123.4); object.put("5", true); object.put("6", new JSONObject(Collections.singletonMap("key", 1))); object.put("7", new JSONArray(Arrays.asList(2))); @@ -369,7 +376,7 @@ public JsonOrgBackwardCompatibleAdapter(String fieldName, TypeAdapter wrapped this.wrappedTypeAdapter = wrappedTypeAdapter; } - protected abstract T createJsonOrgValue(W wrapped); + protected abstract T createJsonOrgValue(W wrapped) throws JSONException; @Override public T read(JsonReader in) throws IOException { @@ -383,7 +390,14 @@ public T read(JsonReader in) throws IOException { if (!name.equals(fieldName)) { throw new IllegalArgumentException("Unexpected name '" + name + "', expected '" + fieldName + "' at " + in.getPath()); } - T value = createJsonOrgValue(wrappedTypeAdapter.read(in)); + T value; + try { + value = createJsonOrgValue(wrappedTypeAdapter.read(in)); + } + // For Android this is a checked exception; for the latest JSON-java artifacts it isn't anymore + catch (JSONException e) { + throw new RuntimeException(e); + } in.endObject(); return value; @@ -435,11 +449,13 @@ public void write(JsonWriter out, T value) throws IOException { TypeAdapter> wrappedAdapter = gson.getAdapter(new TypeToken> () {}); adapter = new JsonOrgBackwardCompatibleAdapter, JSONArray>("myArrayList", wrappedAdapter) { @Override - protected JSONArray createJsonOrgValue(List wrapped) { - JSONArray jsonArray = new JSONArray(wrapped.size()); - // Unlike JSONArray(Collection) constructor, putAll does not wrap elements and is therefore closer + protected JSONArray createJsonOrgValue(List wrapped) throws JSONException { + JSONArray jsonArray = new JSONArray(); + // Unlike JSONArray(Collection) constructor, `put` does not wrap elements and is therefore closer // to original Gson reflection-based behavior - jsonArray.putAll(wrapped); + for (Object element : wrapped) { + jsonArray.put(element); + } return jsonArray; } @@ -448,7 +464,9 @@ protected JSONArray createJsonOrgValue(List wrapped) { protected List getWrapped(JSONArray jsonArray) { // Cannot use JSONArray.toList() because that converts elements List list = new ArrayList<>(jsonArray.length()); - for (Object element : jsonArray) { + for (int i = 0; i < jsonArray.length(); i++) { + // Use opt(int) because get(int) cannot handle null values + Object element = jsonArray.opt(i); list.add(element); } @@ -459,7 +477,7 @@ protected List getWrapped(JSONArray jsonArray) { TypeAdapter> wrappedAdapter = gson.getAdapter(new TypeToken> () {}); adapter = new JsonOrgBackwardCompatibleAdapter, JSONObject>("map", wrappedAdapter) { @Override - protected JSONObject createJsonOrgValue(Map map) { + protected JSONObject createJsonOrgValue(Map map) throws JSONException { // JSONObject(Map) constructor wraps elements, so instead put elements separately to be closer // to original Gson reflection-based behavior JSONObject jsonObject = new JSONObject(); @@ -474,7 +492,10 @@ protected JSONObject createJsonOrgValue(Map map) { protected Map getWrapped(JSONObject jsonObject) { // Cannot use JSONObject.toMap() because that converts elements Map map = new LinkedHashMap<>(jsonObject.length()); - for (String name : jsonObject.keySet()) { + @SuppressWarnings("unchecked") // Old JSON-java versions return just `Iterator` instead of `Iterator` + Iterator names = jsonObject.keys(); + while (names.hasNext()) { + String name = names.next(); // Use opt(String) because get(String) cannot handle null values // Most likely null values cannot occur normally though; they would be JSONObject.NULL map.put(name, jsonObject.opt(name)); @@ -501,7 +522,7 @@ protected Map getWrapped(JSONObject jsonObject) { * as expected. */ @Test - public void testCustomBackwardCompatibleAdapters() { + public void testCustomBackwardCompatibleAdapters() throws JSONException { Gson gson = new GsonBuilder() .serializeNulls() .registerTypeAdapterFactory(new JsonOrgBackwardCompatibleAdapterFactory()) @@ -510,7 +531,7 @@ public void testCustomBackwardCompatibleAdapters() { JSONArray array = new JSONArray(Arrays.asList( null, JSONObject.NULL, - new BigDecimal("123.4"), + 123.4, true, new JSONObject(Collections.singletonMap("key", 1)), new JSONArray(Arrays.asList(2)), @@ -519,12 +540,12 @@ public void testCustomBackwardCompatibleAdapters() { new boolean[] {false} )); assertThat(gson.toJson(array)).isEqualTo( - "{\"myArrayList\":[null,null,123.4,true,{\"map\":{\"key\":1}},{\"myArrayList\":[2]},{\"map\":{\"key\":3}},{\"myArrayList\":[4]},{\"myArrayList\":[false]}]}"); + "{\"myArrayList\":[null,null,123.4,true,{\"map\":{\"key\":1}},{\"myArrayList\":[2]},{\"key\":3},[4],[false]]}"); assertThat(gson.toJson(null, JSONArray.class)).isEqualTo("null"); JSONObject object = new JSONObject(); object.put("1", JSONObject.NULL); - object.put("2", new BigDecimal("123.4")); + object.put("2", 123.4); object.put("3", true); object.put("4", new JSONObject(Collections.singletonMap("key", 1))); object.put("5", new JSONArray(Arrays.asList(2))); From 51f4a769123d02f6bed78279430e6a16ce564aaf Mon Sep 17 00:00:00 2001 From: Marcono1234 Date: Wed, 26 Jul 2023 21:32:09 +0200 Subject: [PATCH 3/8] Add test that only requesting adapter does not throw --- .../java/com/google/gson/functional/JsonOrgInteropTest.java | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/gson/src/test/java/com/google/gson/functional/JsonOrgInteropTest.java b/gson/src/test/java/com/google/gson/functional/JsonOrgInteropTest.java index 567408f5ea..0579bd7143 100644 --- a/gson/src/test/java/com/google/gson/functional/JsonOrgInteropTest.java +++ b/gson/src/test/java/com/google/gson/functional/JsonOrgInteropTest.java @@ -42,6 +42,11 @@ public class JsonOrgInteropTest { @Test public void testNoCustomAdapter() { Gson gson = new Gson(); + + // Merely requesting the adapter should not throw an exception + assertThat(gson.getAdapter(JSONArray.class)).isNotNull(); + assertThat(gson.getAdapter(JSONObject.class)).isNotNull(); + String expectedMessageArray = "Unsupported class from other JSON library: org.json.JSONArray" + "\nSee https://github.com/google/gson/blob/main/Troubleshooting.md#unsupported-json-library-class"; String expectedMessageObject = "Unsupported class from other JSON library: org.json.JSONObject" From 19f34feebe9940316f103eadccd986a08b46a9b6 Mon Sep 17 00:00:00 2001 From: Marcono1234 Date: Sat, 29 Jul 2023 14:49:51 +0200 Subject: [PATCH 4/8] Add copyright header --- ...UnsupportedJsonLibraryTypeAdapterFactory.java | 16 ++++++++++++++++ .../gson/functional/JsonOrgInteropTest.java | 16 ++++++++++++++++ 2 files changed, 32 insertions(+) diff --git a/gson/src/main/java/com/google/gson/internal/bind/UnsupportedJsonLibraryTypeAdapterFactory.java b/gson/src/main/java/com/google/gson/internal/bind/UnsupportedJsonLibraryTypeAdapterFactory.java index d6a1382c17..f4c27a9f1c 100644 --- a/gson/src/main/java/com/google/gson/internal/bind/UnsupportedJsonLibraryTypeAdapterFactory.java +++ b/gson/src/main/java/com/google/gson/internal/bind/UnsupportedJsonLibraryTypeAdapterFactory.java @@ -1,3 +1,19 @@ +/* + * Copyright (C) 2023 Google Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + package com.google.gson.internal.bind; import com.google.gson.Gson; diff --git a/gson/src/test/java/com/google/gson/functional/JsonOrgInteropTest.java b/gson/src/test/java/com/google/gson/functional/JsonOrgInteropTest.java index 0579bd7143..5c3f7fddb3 100644 --- a/gson/src/test/java/com/google/gson/functional/JsonOrgInteropTest.java +++ b/gson/src/test/java/com/google/gson/functional/JsonOrgInteropTest.java @@ -1,3 +1,19 @@ +/* + * Copyright (C) 2023 Google Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + package com.google.gson.functional; import static com.google.common.truth.Truth.assertAbout; From 002e249f494938c962ff876abdc50fda1474530b Mon Sep 17 00:00:00 2001 From: Marcono1234 Date: Sat, 29 Jul 2023 15:11:52 +0200 Subject: [PATCH 5/8] Add back accidentally removed sentence --- Troubleshooting.md | 2 ++ 1 file changed, 2 insertions(+) diff --git a/Troubleshooting.md b/Troubleshooting.md index 3bc40135f8..eb199e8c0a 100644 --- a/Troubleshooting.md +++ b/Troubleshooting.md @@ -52,6 +52,8 @@ module mymodule { } ``` +Or in case this occurs for a field in one of your classes which you did not actually want to serialize or deserialize in the first place, you can exclude that field, see the [user guide](UserGuide.md#excluding-fields-from-serialization-and-deserialization). + ## `RuntimeException`: 'Unsupported class from other JSON library: ...' **Symptom:** An exception with a message in the form 'Unsupported class from other JSON library: ...' is thrown From f358319f0837386d07b003e569e13948b3227cd0 Mon Sep 17 00:00:00 2001 From: Marcono1234 Date: Sat, 29 Jul 2023 15:14:44 +0200 Subject: [PATCH 6/8] Account for different internal field names for JSON-java and Android --- Troubleshooting.md | 48 +++++++++++++++++-- .../gson/functional/JsonOrgInteropTest.java | 45 +++++++++++++++-- 2 files changed, 85 insertions(+), 8 deletions(-) diff --git a/Troubleshooting.md b/Troubleshooting.md index eb199e8c0a..0bf88ffbce 100644 --- a/Troubleshooting.md +++ b/Troubleshooting.md @@ -176,7 +176,7 @@ If you cannot switch the classes you are using, see the library-specific solutio */ public class JsonOrgBackwardCompatibleAdapterFactory implements TypeAdapterFactory { private abstract static class JsonOrgBackwardCompatibleAdapter extends TypeAdapter { - /** Internal field name used by JSON-java for the respective JSON value class */ + /** Internal field name used by JSON-java / Android for the respective JSON value class */ private final String fieldName; private final TypeAdapter wrappedTypeAdapter; @@ -228,6 +228,24 @@ If you cannot switch the classes you are using, see the library-specific solutio } } + /** + * For multiple alternative field names, tries to find the first which exists on the class. + */ + private static String getFieldName(Class c, String... names) throws NoSuchFieldException { + NoSuchFieldException exception = null; + + for (String name : names) { + try { + Field unused = c.getDeclaredField(name); + return name; + } catch (NoSuchFieldException e) { + exception = e; + } + } + + throw exception; + } + @Override public TypeAdapter create(Gson gson, TypeToken type) { Class rawType = type.getRawType(); @@ -255,8 +273,18 @@ If you cannot switch the classes you are using, see the library-specific solutio TypeAdapter adapter; if (rawType == JSONArray.class) { + // Choose correct field name depending on whether JSON-java or Android is used + String fieldName; + try { + String jsonJavaName = "myArrayList"; + String androidName = "values"; + fieldName = getFieldName(JSONArray.class, jsonJavaName, androidName); + } catch (NoSuchFieldException e) { + throw new RuntimeException("Unable to get internal field name for JSONArray", e); + } + TypeAdapter> wrappedAdapter = gson.getAdapter(new TypeToken> () {}); - adapter = new JsonOrgBackwardCompatibleAdapter, JSONArray>("myArrayList", wrappedAdapter) { + adapter = new JsonOrgBackwardCompatibleAdapter, JSONArray>(fieldName, wrappedAdapter) { @Override protected JSONArray createJsonOrgValue(List wrapped) throws JSONException { JSONArray jsonArray = new JSONArray(); @@ -283,8 +311,18 @@ If you cannot switch the classes you are using, see the library-specific solutio } }; } else { + // Choose correct field name depending on whether JSON-java or Android is used + String fieldName; + try { + String jsonJavaName = "map"; + String androidName = "nameValuePairs"; + fieldName = getFieldName(JSONObject.class, jsonJavaName, androidName); + } catch (NoSuchFieldException e) { + throw new RuntimeException("Unable to get internal field name for JSONObject", e); + } + TypeAdapter> wrappedAdapter = gson.getAdapter(new TypeToken> () {}); - adapter = new JsonOrgBackwardCompatibleAdapter, JSONObject>("map", wrappedAdapter) { + adapter = new JsonOrgBackwardCompatibleAdapter, JSONObject>(fieldName, wrappedAdapter) { @Override protected JSONObject createJsonOrgValue(Map map) throws JSONException { // JSONObject(Map) constructor wraps elements, so instead put elements separately to be closer @@ -323,10 +361,10 @@ If you cannot switch the classes you are using, see the library-specific solutio } ``` - **Important:** Verify carefully that these `TypeAdapterFactory` classes work as expected for your use case and produce the desired JSON data or parse the JSON data without issues. There might be corner cases where they behave slightly differently than Gson's reflection-based adapter, respectively behave differently than the other JSON library would behave. - +**Important:** Verify carefully that these `TypeAdapterFactory` classes work as expected for your use case and produce the desired JSON data or parse the JSON data without issues. There might be corner cases where they behave slightly differently than Gson's reflection-based adapter, respectively behave differently than the other JSON library would behave. + ## Android app not working in Release mode; random property names **Symptom:** Your Android app is working fine in Debug mode but fails in Release mode and the JSON properties have seemingly random names such as `a`, `b`, ... diff --git a/gson/src/test/java/com/google/gson/functional/JsonOrgInteropTest.java b/gson/src/test/java/com/google/gson/functional/JsonOrgInteropTest.java index 5c3f7fddb3..9b96e9a2a7 100644 --- a/gson/src/test/java/com/google/gson/functional/JsonOrgInteropTest.java +++ b/gson/src/test/java/com/google/gson/functional/JsonOrgInteropTest.java @@ -35,6 +35,7 @@ import com.google.gson.stream.JsonToken; import com.google.gson.stream.JsonWriter; import java.io.IOException; +import java.lang.reflect.Field; import java.util.ArrayList; import java.util.Arrays; import java.util.Collections; @@ -388,7 +389,7 @@ public void testCustomAdapters() throws JSONException { */ private static class JsonOrgBackwardCompatibleAdapterFactory implements TypeAdapterFactory { private abstract static class JsonOrgBackwardCompatibleAdapter extends TypeAdapter { - /** Internal field name used by JSON-java for the respective JSON value class */ + /** Internal field name used by JSON-java / Android for the respective JSON value class */ private final String fieldName; private final TypeAdapter wrappedTypeAdapter; @@ -440,6 +441,24 @@ public void write(JsonWriter out, T value) throws IOException { } } + /** + * For multiple alternative field names, tries to find the first which exists on the class. + */ + private static String getFieldName(Class c, String... names) throws NoSuchFieldException { + NoSuchFieldException exception = null; + + for (String name : names) { + try { + Field unused = c.getDeclaredField(name); + return name; + } catch (NoSuchFieldException e) { + exception = e; + } + } + + throw exception; + } + @Override public TypeAdapter create(Gson gson, TypeToken type) { Class rawType = type.getRawType(); @@ -467,8 +486,18 @@ public void write(JsonWriter out, T value) throws IOException { TypeAdapter adapter; if (rawType == JSONArray.class) { + // Choose correct field name depending on whether JSON-java or Android is used + String fieldName; + try { + String jsonJavaName = "myArrayList"; + String androidName = "values"; + fieldName = getFieldName(JSONArray.class, jsonJavaName, androidName); + } catch (NoSuchFieldException e) { + throw new RuntimeException("Unable to get internal field name for JSONArray", e); + } + TypeAdapter> wrappedAdapter = gson.getAdapter(new TypeToken> () {}); - adapter = new JsonOrgBackwardCompatibleAdapter, JSONArray>("myArrayList", wrappedAdapter) { + adapter = new JsonOrgBackwardCompatibleAdapter, JSONArray>(fieldName, wrappedAdapter) { @Override protected JSONArray createJsonOrgValue(List wrapped) throws JSONException { JSONArray jsonArray = new JSONArray(); @@ -495,8 +524,18 @@ protected List getWrapped(JSONArray jsonArray) { } }; } else { + // Choose correct field name depending on whether JSON-java or Android is used + String fieldName; + try { + String jsonJavaName = "map"; + String androidName = "nameValuePairs"; + fieldName = getFieldName(JSONObject.class, jsonJavaName, androidName); + } catch (NoSuchFieldException e) { + throw new RuntimeException("Unable to get internal field name for JSONObject", e); + } + TypeAdapter> wrappedAdapter = gson.getAdapter(new TypeToken> () {}); - adapter = new JsonOrgBackwardCompatibleAdapter, JSONObject>("map", wrappedAdapter) { + adapter = new JsonOrgBackwardCompatibleAdapter, JSONObject>(fieldName, wrappedAdapter) { @Override protected JSONObject createJsonOrgValue(Map map) throws JSONException { // JSONObject(Map) constructor wraps elements, so instead put elements separately to be closer From 410d1b766c9b22d24fc1e665c8429cf471aa007f Mon Sep 17 00:00:00 2001 From: Marcono1234 Date: Sat, 29 Jul 2023 15:36:47 +0200 Subject: [PATCH 7/8] Minor documentation changes --- Troubleshooting.md | 2 +- .../bind/UnsupportedJsonLibraryTypeAdapterFactory.java | 6 +++--- .../java/com/google/gson/functional/JsonOrgInteropTest.java | 1 - 3 files changed, 4 insertions(+), 5 deletions(-) diff --git a/Troubleshooting.md b/Troubleshooting.md index 0bf88ffbce..c656857fa7 100644 --- a/Troubleshooting.md +++ b/Troubleshooting.md @@ -363,7 +363,7 @@ If you cannot switch the classes you are using, see the library-specific solutio -**Important:** Verify carefully that these `TypeAdapterFactory` classes work as expected for your use case and produce the desired JSON data or parse the JSON data without issues. There might be corner cases where they behave slightly differently than Gson's reflection-based adapter, respectively behave differently than the other JSON library would behave. +**Important:** Verify carefully that these solutions work as expected for your use case and produce the desired JSON data or parse the JSON data without issues. There might be corner cases where they behave slightly differently than Gson's reflection-based adapter, respectively behave differently than the other JSON library would behave. ## Android app not working in Release mode; random property names diff --git a/gson/src/main/java/com/google/gson/internal/bind/UnsupportedJsonLibraryTypeAdapterFactory.java b/gson/src/main/java/com/google/gson/internal/bind/UnsupportedJsonLibraryTypeAdapterFactory.java index f4c27a9f1c..0a08cb79e4 100644 --- a/gson/src/main/java/com/google/gson/internal/bind/UnsupportedJsonLibraryTypeAdapterFactory.java +++ b/gson/src/main/java/com/google/gson/internal/bind/UnsupportedJsonLibraryTypeAdapterFactory.java @@ -32,9 +32,9 @@ * {@code TypeAdapterFactory} which throws an exception when trying to serialize or * deserialize unsupported classes from third-party JSON libraries. * - *

This is mainly intended as help for users who accidentally mix Gson and non-Gson - * code and are then surprised by unexpected JSON data or issues when trying to - * deserialize the JSON data. + *

This is mainly intended as help for users who accidentally mix Gson and other + * JSON libraries and are then surprised by unexpected JSON data or issues when trying + * to deserialize the JSON data. */ public class UnsupportedJsonLibraryTypeAdapterFactory implements TypeAdapterFactory { public static final UnsupportedJsonLibraryTypeAdapterFactory INSTANCE = new UnsupportedJsonLibraryTypeAdapterFactory(); diff --git a/gson/src/test/java/com/google/gson/functional/JsonOrgInteropTest.java b/gson/src/test/java/com/google/gson/functional/JsonOrgInteropTest.java index 9b96e9a2a7..e9010060d6 100644 --- a/gson/src/test/java/com/google/gson/functional/JsonOrgInteropTest.java +++ b/gson/src/test/java/com/google/gson/functional/JsonOrgInteropTest.java @@ -113,7 +113,6 @@ public String toString() { } private abstract static class JsonOrgBaseSubject extends Subject { - protected JsonOrgBaseSubject(FailureMetadata metadata, @Nullable Object actual) { super(metadata, actual); } From dc6bf5408582bc7f86af0e98941253e09269c9e2 Mon Sep 17 00:00:00 2001 From: Marcono1234 Date: Sat, 29 Jul 2023 15:56:15 +0200 Subject: [PATCH 8/8] Add assertion to avoid warning about dereferencing null for `throw` statement --- Troubleshooting.md | 1 + .../test/java/com/google/gson/functional/JsonOrgInteropTest.java | 1 + 2 files changed, 2 insertions(+) diff --git a/Troubleshooting.md b/Troubleshooting.md index c656857fa7..b8dafd218b 100644 --- a/Troubleshooting.md +++ b/Troubleshooting.md @@ -232,6 +232,7 @@ If you cannot switch the classes you are using, see the library-specific solutio * For multiple alternative field names, tries to find the first which exists on the class. */ private static String getFieldName(Class c, String... names) throws NoSuchFieldException { + assert(names.length > 0); NoSuchFieldException exception = null; for (String name : names) { diff --git a/gson/src/test/java/com/google/gson/functional/JsonOrgInteropTest.java b/gson/src/test/java/com/google/gson/functional/JsonOrgInteropTest.java index e9010060d6..1fc6acbe98 100644 --- a/gson/src/test/java/com/google/gson/functional/JsonOrgInteropTest.java +++ b/gson/src/test/java/com/google/gson/functional/JsonOrgInteropTest.java @@ -444,6 +444,7 @@ public void write(JsonWriter out, T value) throws IOException { * For multiple alternative field names, tries to find the first which exists on the class. */ private static String getFieldName(Class c, String... names) throws NoSuchFieldException { + assert(names.length > 0); NoSuchFieldException exception = null; for (String name : names) {