Skip to content

Commit

Permalink
Throw exception when using classes from other JSON libraries with Gson
Browse files Browse the repository at this point in the history
  • Loading branch information
Marcono1234 committed Jul 26, 2023
1 parent a38b757 commit 920366e
Show file tree
Hide file tree
Showing 5 changed files with 901 additions and 1 deletion.
253 changes: 252 additions & 1 deletion Troubleshooting.md
Original file line number Diff line number Diff line change
Expand Up @@ -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).
## <a id="unsupported-json-library-class"></a>`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)
<details>

<summary>(Click to show)</summary>

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)):

<!-- Important: Make sure the code below is in sync with the code in JsonOrgInteropTest -->
```java
/**
* {@code TypeAdapterFactory} for {@link JSONArray} and {@link JSONObject}.
*
* <p>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<T> extends TypeAdapter<T> {
private final TypeAdapter<JsonElement> jsonElementAdapter;

public JsonOrgAdapter(TypeAdapter<JsonElement> 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 <T> TypeAdapter<T> create(Gson gson, TypeToken<T> type) {
Class<?> rawType = type.getRawType();
if (rawType != JSONArray.class && rawType != JSONObject.class) {
return null;
}

TypeAdapter<JsonElement> jsonElementAdapter = gson.getAdapter(JsonElement.class);

TypeAdapter<?> adapter;
if (rawType == JSONArray.class) {
adapter = new JsonOrgAdapter<JSONArray>(jsonElementAdapter) {
@Override
protected JSONArray readJsonOrgValue(String json) {
return new JSONArray(json);
}
};
} else {
adapter = new JsonOrgAdapter<JSONObject>(jsonElementAdapter) {
@Override
protected JSONObject readJsonOrgValue(String json) {
return new JSONObject(json);
}
};
}

// Safe due to type check at beginning of method
@SuppressWarnings("unchecked")
TypeAdapter<T> t = (TypeAdapter<T>) 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:
<!-- Important: Make sure the code below is in sync with the code in JsonOrgInteropTest -->
```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.
*
* <p>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<W, T> extends TypeAdapter<T> {
/** Internal field name used by JSON-java for the respective JSON value class */
private final String fieldName;
private final TypeAdapter<W> wrappedTypeAdapter;
public JsonOrgBackwardCompatibleAdapter(String fieldName, TypeAdapter<W> 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 <T> TypeAdapter<T> create(Gson gson, TypeToken<T> 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<T>() {
@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<List<Object>> wrappedAdapter = gson.getAdapter(new TypeToken<List<Object>> () {});
adapter = new JsonOrgBackwardCompatibleAdapter<List<Object>, JSONArray>("myArrayList", wrappedAdapter) {
@Override
protected JSONArray createJsonOrgValue(List<Object> 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<Object> getWrapped(JSONArray jsonArray) {
// Cannot use JSONArray.toList() because that converts elements
List<Object> list = new ArrayList<>(jsonArray.length());
for (Object element : jsonArray) {
list.add(element);
}
return list;
}
};
} else {
TypeAdapter<Map<String, Object>> wrappedAdapter = gson.getAdapter(new TypeToken<Map<String, Object>> () {});
adapter = new JsonOrgBackwardCompatibleAdapter<Map<String, Object>, JSONObject>("map", wrappedAdapter) {
@Override
protected JSONObject createJsonOrgValue(Map<String, Object> 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<String, Object> entry : map.entrySet()) {
jsonObject.put(entry.getKey(), entry.getValue());
}
return jsonObject;
}
@Override
protected Map<String, Object> getWrapped(JSONObject jsonObject) {
// Cannot use JSONObject.toMap() because that converts elements
Map<String, Object> 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> t = (TypeAdapter<T>) 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.

</details>

## <a id="android-app-random-names"></a> Android app not working in Release mode; random property names

Expand Down
8 changes: 8 additions & 0 deletions gson/pom.xml
Original file line number Diff line number Diff line change
Expand Up @@ -49,6 +49,14 @@
<version>2.20.0</version>
</dependency>

<!-- For testing interoperability with other JSON libraries -->
<dependency>
<groupId>org.json</groupId>
<artifactId>json</artifactId>
<version>20230618</version>
<scope>test</scope>
</dependency>

<dependency>
<groupId>junit</groupId>
<artifactId>junit</artifactId>
Expand Down
4 changes: 4 additions & 0 deletions gson/src/main/java/com/google/gson/Gson.java
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -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));

Expand Down
Original file line number Diff line number Diff line change
@@ -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.
*
* <p>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<String> 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 <T> TypeAdapter<T> create(Gson gson, TypeToken<T> 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<T>() {
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();
}
};
}
}
Loading

0 comments on commit 920366e

Please sign in to comment.