Skip to content
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

Improve settings propagation for temporary JsonTreeReader and JsonTreeWriter #2151

Draft
wants to merge 5 commits into
base: main
Choose a base branch
from
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -194,7 +194,7 @@ public <T> TypeAdapter<T> create(Gson gson, TypeToken<T> type) {
// now that we know the typeAdapter for this name, go from JsonElement to 'T'
if (element.value == null) {
element.typeAdapter = typeAdapter;
element.read(graph);
element.read(graph, in);
}
return element.value;
} finally {
Expand Down Expand Up @@ -299,12 +299,12 @@ void write(JsonWriter out) throws IOException {
typeAdapter.write(out, value);
}

void read(Graph graph) throws IOException {
void read(Graph graph, JsonReader originalReader) throws IOException {
if (graph.nextCreate != null) {
throw new IllegalStateException("Unexpected recursive call to read() for " + id);
}
graph.nextCreate = this;
value = typeAdapter.fromJsonTree(element);
value = typeAdapter.fromJsonTreeWithSettingsFrom(element, originalReader);
if (value == null) {
throw new IllegalStateException("non-null value deserialized to null: " + element);
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -151,12 +151,15 @@ private RuntimeTypeAdapterFactory(Class<?> baseType, String typeFieldName, boole
/**
* Creates a new runtime type adapter using for {@code baseType} using {@code
* typeFieldName} as the type field name. Type field names are case sensitive.
* {@code maintainType} flag decide if the type will be stored in pojo or not.
* {@code maintainType} flag decides if during deserialization the type field
* is kept ({@code true}) or removed ({@code false}), and whether during
* serialization it is added by this factory ({@code false}) or assumed to be
* already present for the class ({@code true}).
*/
public static <T> RuntimeTypeAdapterFactory<T> of(Class<T> baseType, String typeFieldName, boolean maintainType) {
return new RuntimeTypeAdapterFactory<>(baseType, typeFieldName, maintainType);
}

/**
* Creates a new runtime type adapter using for {@code baseType} using {@code
* typeFieldName} as the type field name. Type field names are case sensitive.
Expand Down Expand Up @@ -227,7 +230,7 @@ public <R> TypeAdapter<R> create(Gson gson, TypeToken<R> type) {
} else {
labelJsonElement = jsonElement.getAsJsonObject().remove(typeFieldName);
}

if (labelJsonElement == null) {
throw new JsonParseException("cannot deserialize " + baseType
+ " because it does not define a field named " + typeFieldName);
Expand All @@ -239,7 +242,7 @@ public <R> TypeAdapter<R> create(Gson gson, TypeToken<R> type) {
throw new JsonParseException("cannot deserialize " + baseType + " subtype named "
+ label + "; did you forget to register a subtype?");
}
return delegate.fromJsonTree(jsonElement);
return delegate.fromJsonTreeWithSettingsFrom(jsonElement, in);
}

@Override public void write(JsonWriter out, R value) throws IOException {
Expand All @@ -251,10 +254,10 @@ public <R> TypeAdapter<R> create(Gson gson, TypeToken<R> type) {
throw new JsonParseException("cannot serialize " + srcType.getName()
+ "; did you forget to register a subtype?");
}
JsonObject jsonObject = delegate.toJsonTree(value).getAsJsonObject();
JsonObject jsonObject = delegate.toJsonTreeWithSettingsFrom(value, out).getAsJsonObject();

if (maintainType) {
jsonElementAdapter.write(out, jsonObject);
writeObjectPermissively(out, jsonObject);
return;
}

Expand All @@ -265,11 +268,37 @@ public <R> TypeAdapter<R> create(Gson gson, TypeToken<R> type) {
+ " because it already defines a field named " + typeFieldName);
}
clone.add(typeFieldName, new JsonPrimitive(label));

for (Map.Entry<String, JsonElement> e : jsonObject.entrySet()) {
clone.add(e.getKey(), e.getValue());
}
jsonElementAdapter.write(out, clone);
writeObjectPermissively(out, clone);
}

private void writeObjectPermissively(JsonWriter out, JsonObject object) throws IOException {
/*
* When object was written to JsonObject its adapter might have temporarily overwritten
* JsonWriter settings. Cannot know which settings it used, therefore when writing
* JsonObject here, make it as permissive as possible.
*
* This has no effect if adapter did not change settings. Then JsonObject was written
* with same settings as `out` and the following temporary settings changes won't make
* a difference (assuming JsonTreeWriter and JsonWriter both handle the settings in the
* same way).
*
* Unfortunately this workaround won't work for HTML-safe and indentation settings,
* though at least they do not affect the JSON data, only the formatting.
*/
boolean oldLenient = out.isLenient();
boolean oldSerializeNulls = out.getSerializeNulls();
try {
out.setLenient(true);
out.setSerializeNulls(true);
jsonElementAdapter.write(out, object);
} finally {
out.setLenient(oldLenient);
out.setSerializeNulls(oldSerializeNulls);
}
}
}.nullSafe();
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -18,18 +18,21 @@

import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertSame;
import static org.junit.Assert.fail;

import com.google.gson.Gson;
import com.google.gson.GsonBuilder;
import com.google.gson.TypeAdapter;
import com.google.gson.reflect.TypeToken;
import com.google.gson.stream.JsonReader;
import java.io.IOException;
import java.io.StringReader;
import java.lang.reflect.Type;
import java.util.ArrayList;
import java.util.Collections;
import java.util.List;

import org.junit.Test;

import com.google.gson.Gson;
import com.google.gson.GsonBuilder;
import com.google.gson.reflect.TypeToken;

public final class GraphAdapterBuilderTest {
@Test
public void testSerialization() {
Expand Down Expand Up @@ -88,6 +91,35 @@ public void testDeserializationDirectSelfReference() {
assertSame(suicide, suicide.beats);
}

static class DoubleContainer {
double d;
}

@Test
public void testDeserializationLenientness() throws IOException {
String json = "{\"0x1\":{\"d\":\"NaN\"}}";

GsonBuilder gsonBuilder = new GsonBuilder();
new GraphAdapterBuilder()
.addType(DoubleContainer.class)
.registerOn(gsonBuilder);
// Use TypeAdapter to avoid default lenientness of Gson
TypeAdapter<DoubleContainer> adapter = gsonBuilder.create().getAdapter(DoubleContainer.class);

try {
adapter.read(new JsonReader(new StringReader(json)));
fail();
} catch (IllegalArgumentException e) {
assertEquals("JSON forbids NaN and infinities: NaN", e.getMessage());
}

JsonReader lenientReader = new JsonReader(new StringReader(json));
lenientReader.setLenient(true);

DoubleContainer deserialized = adapter.read(lenientReader);
assertEquals((Double) Double.NaN, (Double) deserialized.d);
}

@Test
public void testSerializeListOfLists() {
Type listOfListsType = new TypeToken<List<List<?>>>() {}.getType();
Expand Down