817 lines
28 KiB
Java
817 lines
28 KiB
Java
/*
|
|
* Copyright (C) 2014 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.assertThat;
|
|
import static org.junit.Assert.fail;
|
|
|
|
import com.google.common.base.Splitter;
|
|
import com.google.gson.Gson;
|
|
import com.google.gson.GsonBuilder;
|
|
import com.google.gson.InstanceCreator;
|
|
import com.google.gson.JsonDeserializationContext;
|
|
import com.google.gson.JsonDeserializer;
|
|
import com.google.gson.JsonElement;
|
|
import com.google.gson.JsonParseException;
|
|
import com.google.gson.JsonPrimitive;
|
|
import com.google.gson.JsonSerializationContext;
|
|
import com.google.gson.JsonSerializer;
|
|
import com.google.gson.TypeAdapter;
|
|
import com.google.gson.TypeAdapterFactory;
|
|
import com.google.gson.annotations.JsonAdapter;
|
|
import com.google.gson.reflect.TypeToken;
|
|
import com.google.gson.stream.JsonReader;
|
|
import com.google.gson.stream.JsonWriter;
|
|
import java.io.IOException;
|
|
import java.lang.reflect.ParameterizedType;
|
|
import java.lang.reflect.Type;
|
|
import java.util.List;
|
|
import java.util.Locale;
|
|
import org.junit.Test;
|
|
|
|
/** Functional tests for the {@link JsonAdapter} annotation on classes. */
|
|
public final class JsonAdapterAnnotationOnClassesTest {
|
|
|
|
@Test
|
|
public void testJsonAdapterInvoked() {
|
|
Gson gson = new Gson();
|
|
String json = gson.toJson(new A("bar"));
|
|
assertThat(json).isEqualTo("\"jsonAdapter\"");
|
|
|
|
// Also invoke the JsonAdapter javadoc sample
|
|
json = gson.toJson(new User("Inderjeet", "Singh"));
|
|
assertThat(json).isEqualTo("{\"name\":\"Inderjeet Singh\"}");
|
|
User user = gson.fromJson("{'name':'Joel Leitch'}", User.class);
|
|
assertThat(user.firstName).isEqualTo("Joel");
|
|
assertThat(user.lastName).isEqualTo("Leitch");
|
|
|
|
json = gson.toJson(Foo.BAR);
|
|
assertThat(json).isEqualTo("\"bar\"");
|
|
Foo baz = gson.fromJson("\"baz\"", Foo.class);
|
|
assertThat(baz).isEqualTo(Foo.BAZ);
|
|
}
|
|
|
|
@Test
|
|
public void testJsonAdapterFactoryInvoked() {
|
|
Gson gson = new Gson();
|
|
String json = gson.toJson(new C("bar"));
|
|
assertThat(json).isEqualTo("\"jsonAdapterFactory\"");
|
|
C c = gson.fromJson("\"bar\"", C.class);
|
|
assertThat(c.value).isEqualTo("jsonAdapterFactory");
|
|
}
|
|
|
|
@Test
|
|
public void testRegisteredAdapterOverridesJsonAdapter() {
|
|
TypeAdapter<A> typeAdapter =
|
|
new TypeAdapter<A>() {
|
|
@Override
|
|
public void write(JsonWriter out, A value) throws IOException {
|
|
out.value("registeredAdapter");
|
|
}
|
|
|
|
@Override
|
|
public A read(JsonReader in) throws IOException {
|
|
return new A(in.nextString());
|
|
}
|
|
};
|
|
Gson gson = new GsonBuilder().registerTypeAdapter(A.class, typeAdapter).create();
|
|
String json = gson.toJson(new A("abcd"));
|
|
assertThat(json).isEqualTo("\"registeredAdapter\"");
|
|
}
|
|
|
|
/** The serializer overrides field adapter, but for deserializer the fieldAdapter is used. */
|
|
@Test
|
|
public void testRegisteredSerializerOverridesJsonAdapter() {
|
|
JsonSerializer<A> serializer =
|
|
new JsonSerializer<A>() {
|
|
@Override
|
|
public JsonElement serialize(A src, Type typeOfSrc, JsonSerializationContext context) {
|
|
return new JsonPrimitive("registeredSerializer");
|
|
}
|
|
};
|
|
Gson gson = new GsonBuilder().registerTypeAdapter(A.class, serializer).create();
|
|
String json = gson.toJson(new A("abcd"));
|
|
assertThat(json).isEqualTo("\"registeredSerializer\"");
|
|
A target = gson.fromJson("abcd", A.class);
|
|
assertThat(target.value).isEqualTo("jsonAdapter");
|
|
}
|
|
|
|
/** The deserializer overrides Json adapter, but for serializer the jsonAdapter is used. */
|
|
@Test
|
|
public void testRegisteredDeserializerOverridesJsonAdapter() {
|
|
JsonDeserializer<A> deserializer =
|
|
new JsonDeserializer<A>() {
|
|
@Override
|
|
public A deserialize(JsonElement json, Type typeOfT, JsonDeserializationContext context)
|
|
throws JsonParseException {
|
|
return new A("registeredDeserializer");
|
|
}
|
|
};
|
|
Gson gson = new GsonBuilder().registerTypeAdapter(A.class, deserializer).create();
|
|
String json = gson.toJson(new A("abcd"));
|
|
assertThat(json).isEqualTo("\"jsonAdapter\"");
|
|
A target = gson.fromJson("abcd", A.class);
|
|
assertThat(target.value).isEqualTo("registeredDeserializer");
|
|
}
|
|
|
|
@Test
|
|
public void testIncorrectTypeAdapterFails() {
|
|
try {
|
|
String json = new Gson().toJson(new ClassWithIncorrectJsonAdapter("bar"));
|
|
fail(json);
|
|
} catch (ClassCastException expected) {
|
|
}
|
|
}
|
|
|
|
@Test
|
|
public void testSuperclassTypeAdapterNotInvoked() {
|
|
String json = new Gson().toJson(new B("bar"));
|
|
assertThat(json).doesNotContain("jsonAdapter");
|
|
}
|
|
|
|
@Test
|
|
public void testNullSafeObject() {
|
|
Gson gson = new Gson();
|
|
NullableClass fromJson = gson.fromJson("null", NullableClass.class);
|
|
assertThat(fromJson).isNull();
|
|
|
|
fromJson = gson.fromJson("\"ignored\"", NullableClass.class);
|
|
assertThat(fromJson).isNotNull();
|
|
|
|
String json = gson.toJson(null, NullableClass.class);
|
|
assertThat(json).isEqualTo("null");
|
|
|
|
json = gson.toJson(new NullableClass());
|
|
assertThat(json).isEqualTo("\"nullable\"");
|
|
}
|
|
|
|
/**
|
|
* Tests behavior when a {@link TypeAdapterFactory} registered with {@code @JsonAdapter} returns
|
|
* {@code null}, indicating that it cannot handle the type and Gson should try a different factory
|
|
* instead.
|
|
*/
|
|
@Test
|
|
public void testFactoryReturningNull() {
|
|
Gson gson = new Gson();
|
|
|
|
assertThat(gson.fromJson("null", WithNullReturningFactory.class)).isNull();
|
|
assertThat(gson.toJson(null, WithNullReturningFactory.class)).isEqualTo("null");
|
|
|
|
TypeToken<WithNullReturningFactory<String>> stringTypeArg =
|
|
new TypeToken<WithNullReturningFactory<String>>() {};
|
|
WithNullReturningFactory<?> deserialized = gson.fromJson("\"a\"", stringTypeArg);
|
|
assertThat(deserialized.t).isEqualTo("custom-read:a");
|
|
assertThat(gson.fromJson("null", stringTypeArg)).isNull();
|
|
assertThat(gson.toJson(new WithNullReturningFactory<>("b"), stringTypeArg.getType()))
|
|
.isEqualTo("\"custom-write:b\"");
|
|
assertThat(gson.toJson(null, stringTypeArg.getType())).isEqualTo("null");
|
|
|
|
// Factory should return `null` for this type and Gson should fall back to reflection-based
|
|
// adapter
|
|
TypeToken<WithNullReturningFactory<Integer>> numberTypeArg =
|
|
new TypeToken<WithNullReturningFactory<Integer>>() {};
|
|
deserialized = gson.fromJson("{\"t\":1}", numberTypeArg);
|
|
assertThat(deserialized.t).isEqualTo(1);
|
|
assertThat(gson.toJson(new WithNullReturningFactory<>(2), numberTypeArg.getType()))
|
|
.isEqualTo("{\"t\":2}");
|
|
}
|
|
|
|
// Also set `nullSafe = true` to verify that this does not cause a NullPointerException if the
|
|
// factory would accidentally call `nullSafe()` on null adapter
|
|
@JsonAdapter(value = WithNullReturningFactory.NullReturningFactory.class, nullSafe = true)
|
|
private static class WithNullReturningFactory<T> {
|
|
T t;
|
|
|
|
public WithNullReturningFactory(T t) {
|
|
this.t = t;
|
|
}
|
|
|
|
static class NullReturningFactory implements TypeAdapterFactory {
|
|
@Override
|
|
public <T> TypeAdapter<T> create(Gson gson, TypeToken<T> type) {
|
|
// Don't handle raw (non-parameterized) type
|
|
if (type.getType() instanceof Class) {
|
|
return null;
|
|
}
|
|
ParameterizedType parameterizedType = (ParameterizedType) type.getType();
|
|
// Makes this test a bit more realistic by only conditionally returning null (instead of
|
|
// always)
|
|
if (parameterizedType.getActualTypeArguments()[0] != String.class) {
|
|
return null;
|
|
}
|
|
|
|
@SuppressWarnings("unchecked")
|
|
TypeAdapter<T> adapter =
|
|
(TypeAdapter<T>)
|
|
new TypeAdapter<WithNullReturningFactory<String>>() {
|
|
@Override
|
|
public void write(JsonWriter out, WithNullReturningFactory<String> value)
|
|
throws IOException {
|
|
out.value("custom-write:" + value.t);
|
|
}
|
|
|
|
@Override
|
|
public WithNullReturningFactory<String> read(JsonReader in) throws IOException {
|
|
return new WithNullReturningFactory<>("custom-read:" + in.nextString());
|
|
}
|
|
};
|
|
return adapter;
|
|
}
|
|
}
|
|
}
|
|
|
|
@JsonAdapter(A.JsonAdapter.class)
|
|
private static class A {
|
|
final String value;
|
|
|
|
A(String value) {
|
|
this.value = value;
|
|
}
|
|
|
|
static final class JsonAdapter extends TypeAdapter<A> {
|
|
@Override
|
|
public void write(JsonWriter out, A value) throws IOException {
|
|
out.value("jsonAdapter");
|
|
}
|
|
|
|
@Override
|
|
public A read(JsonReader in) throws IOException {
|
|
in.nextString();
|
|
return new A("jsonAdapter");
|
|
}
|
|
}
|
|
}
|
|
|
|
@JsonAdapter(C.JsonAdapterFactory.class)
|
|
private static class C {
|
|
final String value;
|
|
|
|
C(String value) {
|
|
this.value = value;
|
|
}
|
|
|
|
static final class JsonAdapterFactory implements TypeAdapterFactory {
|
|
@Override
|
|
public <T> TypeAdapter<T> create(Gson gson, final TypeToken<T> type) {
|
|
return new TypeAdapter<T>() {
|
|
@Override
|
|
public void write(JsonWriter out, T value) throws IOException {
|
|
out.value("jsonAdapterFactory");
|
|
}
|
|
|
|
@SuppressWarnings("unchecked")
|
|
@Override
|
|
public T read(JsonReader in) throws IOException {
|
|
in.nextString();
|
|
return (T) new C("jsonAdapterFactory");
|
|
}
|
|
};
|
|
}
|
|
}
|
|
}
|
|
|
|
private static final class B extends A {
|
|
B(String value) {
|
|
super(value);
|
|
}
|
|
}
|
|
|
|
// Note that the type is NOT TypeAdapter<ClassWithIncorrectJsonAdapter> so this
|
|
// should cause error
|
|
@JsonAdapter(A.JsonAdapter.class)
|
|
private static final class ClassWithIncorrectJsonAdapter {
|
|
@SuppressWarnings("unused")
|
|
final String value;
|
|
|
|
ClassWithIncorrectJsonAdapter(String value) {
|
|
this.value = value;
|
|
}
|
|
}
|
|
|
|
// This class is used in JsonAdapter Javadoc as an example
|
|
@JsonAdapter(UserJsonAdapter.class)
|
|
private static class User {
|
|
final String firstName, lastName;
|
|
|
|
User(String firstName, String lastName) {
|
|
this.firstName = firstName;
|
|
this.lastName = lastName;
|
|
}
|
|
}
|
|
|
|
private static class UserJsonAdapter extends TypeAdapter<User> {
|
|
@Override
|
|
public void write(JsonWriter out, User user) throws IOException {
|
|
// implement write: combine firstName and lastName into name
|
|
out.beginObject();
|
|
out.name("name");
|
|
out.value(user.firstName + " " + user.lastName);
|
|
out.endObject();
|
|
}
|
|
|
|
@Override
|
|
public User read(JsonReader in) throws IOException {
|
|
// implement read: split name into firstName and lastName
|
|
in.beginObject();
|
|
in.nextName();
|
|
List<String> nameParts = Splitter.on(" ").splitToList(in.nextString());
|
|
in.endObject();
|
|
return new User(nameParts.get(0), nameParts.get(1));
|
|
}
|
|
}
|
|
|
|
// Implicit `nullSafe=true`
|
|
@JsonAdapter(value = NullableClassJsonAdapter.class)
|
|
private static class NullableClass {}
|
|
|
|
private static class NullableClassJsonAdapter extends TypeAdapter<NullableClass> {
|
|
@Override
|
|
public void write(JsonWriter out, NullableClass value) throws IOException {
|
|
out.value("nullable");
|
|
}
|
|
|
|
@Override
|
|
public NullableClass read(JsonReader in) throws IOException {
|
|
in.nextString();
|
|
return new NullableClass();
|
|
}
|
|
}
|
|
|
|
@JsonAdapter(FooJsonAdapter.class)
|
|
private static enum Foo {
|
|
BAR,
|
|
BAZ
|
|
}
|
|
|
|
private static class FooJsonAdapter extends TypeAdapter<Foo> {
|
|
@Override
|
|
public void write(JsonWriter out, Foo value) throws IOException {
|
|
out.value(value.name().toLowerCase(Locale.US));
|
|
}
|
|
|
|
@Override
|
|
public Foo read(JsonReader in) throws IOException {
|
|
return Foo.valueOf(in.nextString().toUpperCase(Locale.US));
|
|
}
|
|
}
|
|
|
|
@Test
|
|
public void testIncorrectJsonAdapterType() {
|
|
try {
|
|
new Gson().toJson(new D());
|
|
fail();
|
|
} catch (IllegalArgumentException expected) {
|
|
}
|
|
}
|
|
|
|
@JsonAdapter(Integer.class)
|
|
private static final class D {
|
|
@SuppressWarnings("unused")
|
|
final String value = "a";
|
|
}
|
|
|
|
/**
|
|
* Verifies that {@link TypeAdapterFactory} specified by {@code @JsonAdapter} can call {@link
|
|
* Gson#getDelegateAdapter} without any issues, despite the factory not being directly registered
|
|
* on Gson.
|
|
*/
|
|
@Test
|
|
public void testDelegatingAdapterFactory() {
|
|
@SuppressWarnings("unchecked")
|
|
WithDelegatingFactory<String> deserialized =
|
|
new Gson().fromJson("{\"custom\":{\"f\":\"de\"}}", WithDelegatingFactory.class);
|
|
assertThat(deserialized.f).isEqualTo("de");
|
|
|
|
deserialized =
|
|
new Gson()
|
|
.fromJson(
|
|
"{\"custom\":{\"f\":\"de\"}}", new TypeToken<WithDelegatingFactory<String>>() {});
|
|
assertThat(deserialized.f).isEqualTo("de");
|
|
|
|
WithDelegatingFactory<String> serialized = new WithDelegatingFactory<>("se");
|
|
assertThat(new Gson().toJson(serialized)).isEqualTo("{\"custom\":{\"f\":\"se\"}}");
|
|
}
|
|
|
|
@JsonAdapter(WithDelegatingFactory.Factory.class)
|
|
private static class WithDelegatingFactory<T> {
|
|
T f;
|
|
|
|
WithDelegatingFactory(T f) {
|
|
this.f = f;
|
|
}
|
|
|
|
static class Factory implements TypeAdapterFactory {
|
|
@Override
|
|
public <T> TypeAdapter<T> create(Gson gson, TypeToken<T> type) {
|
|
if (type.getRawType() != WithDelegatingFactory.class) {
|
|
return null;
|
|
}
|
|
|
|
TypeAdapter<T> delegate = gson.getDelegateAdapter(this, type);
|
|
|
|
return new TypeAdapter<T>() {
|
|
@Override
|
|
public T read(JsonReader in) throws IOException {
|
|
// Perform custom deserialization
|
|
in.beginObject();
|
|
assertThat(in.nextName()).isEqualTo("custom");
|
|
T t = delegate.read(in);
|
|
in.endObject();
|
|
|
|
return t;
|
|
}
|
|
|
|
@Override
|
|
public void write(JsonWriter out, T value) throws IOException {
|
|
// Perform custom serialization
|
|
out.beginObject();
|
|
out.name("custom");
|
|
delegate.write(out, value);
|
|
out.endObject();
|
|
}
|
|
};
|
|
}
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Similar to {@link #testDelegatingAdapterFactory}, except that the delegate is not looked up in
|
|
* {@code create} but instead in the adapter methods.
|
|
*/
|
|
@Test
|
|
public void testDelegatingAdapterFactory_Delayed() {
|
|
WithDelayedDelegatingFactory deserialized =
|
|
new Gson().fromJson("{\"custom\":{\"f\":\"de\"}}", WithDelayedDelegatingFactory.class);
|
|
assertThat(deserialized.f).isEqualTo("de");
|
|
|
|
WithDelayedDelegatingFactory serialized = new WithDelayedDelegatingFactory("se");
|
|
assertThat(new Gson().toJson(serialized)).isEqualTo("{\"custom\":{\"f\":\"se\"}}");
|
|
}
|
|
|
|
@JsonAdapter(WithDelayedDelegatingFactory.Factory.class)
|
|
private static class WithDelayedDelegatingFactory {
|
|
String f;
|
|
|
|
WithDelayedDelegatingFactory(String f) {
|
|
this.f = f;
|
|
}
|
|
|
|
static class Factory implements TypeAdapterFactory {
|
|
@Override
|
|
public <T> TypeAdapter<T> create(Gson gson, TypeToken<T> type) {
|
|
return new TypeAdapter<T>() {
|
|
// suppress Error Prone warning; should be clear that `Factory` refers to enclosing class
|
|
@SuppressWarnings("SameNameButDifferent")
|
|
private TypeAdapter<T> delegate() {
|
|
return gson.getDelegateAdapter(Factory.this, type);
|
|
}
|
|
|
|
@Override
|
|
public T read(JsonReader in) throws IOException {
|
|
// Perform custom deserialization
|
|
in.beginObject();
|
|
assertThat(in.nextName()).isEqualTo("custom");
|
|
T t = delegate().read(in);
|
|
in.endObject();
|
|
|
|
return t;
|
|
}
|
|
|
|
@Override
|
|
public void write(JsonWriter out, T value) throws IOException {
|
|
// Perform custom serialization
|
|
out.beginObject();
|
|
out.name("custom");
|
|
delegate().write(out, value);
|
|
out.endObject();
|
|
}
|
|
};
|
|
}
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Tests behavior of {@link Gson#getDelegateAdapter} when <i>different</i> instances of the same
|
|
* factory class are used; one registered on the {@code GsonBuilder} and the other implicitly
|
|
* through {@code @JsonAdapter}.
|
|
*/
|
|
@Test
|
|
public void testDelegating_SameFactoryClass() {
|
|
Gson gson =
|
|
new GsonBuilder().registerTypeAdapterFactory(new WithDelegatingFactory.Factory()).create();
|
|
|
|
// Should use both factories, and therefore have `{"custom": ... }` twice
|
|
WithDelegatingFactory<?> deserialized =
|
|
gson.fromJson("{\"custom\":{\"custom\":{\"f\":\"de\"}}}", WithDelegatingFactory.class);
|
|
assertThat(deserialized.f).isEqualTo("de");
|
|
|
|
WithDelegatingFactory<String> serialized = new WithDelegatingFactory<>("se");
|
|
assertThat(gson.toJson(serialized)).isEqualTo("{\"custom\":{\"custom\":{\"f\":\"se\"}}}");
|
|
}
|
|
|
|
/**
|
|
* Tests behavior of {@link Gson#getDelegateAdapter} when the <i>same</i> instance of a factory is
|
|
* used (through {@link InstanceCreator}).
|
|
*
|
|
* <p><b>Important:</b> This situation is likely a rare corner case; the purpose of this test is
|
|
* to verify that Gson behaves reasonable, mainly that it does not cause a {@link
|
|
* StackOverflowError} due to infinite recursion. This test is not intended to dictate an expected
|
|
* behavior.
|
|
*/
|
|
@Test
|
|
public void testDelegating_SameFactoryInstance() {
|
|
WithDelegatingFactory.Factory factory = new WithDelegatingFactory.Factory();
|
|
|
|
Gson gson =
|
|
new GsonBuilder()
|
|
.registerTypeAdapterFactory(factory)
|
|
// Always provides same instance for factory
|
|
.registerTypeAdapter(
|
|
WithDelegatingFactory.Factory.class, (InstanceCreator<?>) type -> factory)
|
|
.create();
|
|
|
|
// Current Gson.getDelegateAdapter implementation cannot tell when call is related to
|
|
// @JsonAdapter or not, it can only work based on the `skipPast` factory, so if the same factory
|
|
// instance is used the one registered with `GsonBuilder.registerTypeAdapterFactory` actually
|
|
// skips past the @JsonAdapter one, so the JSON string is `{"custom": ...}` instead of
|
|
// `{"custom":{"custom":...}}`
|
|
WithDelegatingFactory<?> deserialized =
|
|
gson.fromJson("{\"custom\":{\"f\":\"de\"}}", WithDelegatingFactory.class);
|
|
assertThat(deserialized.f).isEqualTo("de");
|
|
|
|
WithDelegatingFactory<String> serialized = new WithDelegatingFactory<>("se");
|
|
assertThat(gson.toJson(serialized)).isEqualTo("{\"custom\":{\"f\":\"se\"}}");
|
|
}
|
|
|
|
/**
|
|
* Tests behavior of {@link Gson#getDelegateAdapter} when <i>different</i> instances of the same
|
|
* factory class are used; one specified with {@code @JsonAdapter} on a class, and the other
|
|
* specified with {@code @JsonAdapter} on a field of that class.
|
|
*
|
|
* <p><b>Important:</b> This situation is likely a rare corner case; the purpose of this test is
|
|
* to verify that Gson behaves reasonable, mainly that it does not cause a {@link
|
|
* StackOverflowError} due to infinite recursion. This test is not intended to dictate an expected
|
|
* behavior.
|
|
*/
|
|
@Test
|
|
public void testDelegating_SameFactoryClass_OnClassAndField() {
|
|
Gson gson =
|
|
new GsonBuilder()
|
|
.registerTypeAdapter(
|
|
String.class,
|
|
new TypeAdapter<String>() {
|
|
@Override
|
|
public String read(JsonReader in) throws IOException {
|
|
return in.nextString() + "-str";
|
|
}
|
|
|
|
@Override
|
|
public void write(JsonWriter out, String value) throws IOException {
|
|
out.value(value + "-str");
|
|
}
|
|
})
|
|
.create();
|
|
|
|
// Should use both factories, and therefore have `{"custom": ... }` once for class and once for
|
|
// the field, and for field also properly delegate to custom String adapter defined above
|
|
WithDelegatingFactoryOnClassAndField deserialized =
|
|
gson.fromJson(
|
|
"{\"custom\":{\"f\":{\"custom\":\"de\"}}}", WithDelegatingFactoryOnClassAndField.class);
|
|
assertThat(deserialized.f).isEqualTo("de-str");
|
|
|
|
WithDelegatingFactoryOnClassAndField serialized =
|
|
new WithDelegatingFactoryOnClassAndField("se");
|
|
assertThat(gson.toJson(serialized)).isEqualTo("{\"custom\":{\"f\":{\"custom\":\"se-str\"}}}");
|
|
}
|
|
|
|
/**
|
|
* Tests behavior of {@link Gson#getDelegateAdapter} when the <i>same</i> instance of a factory is
|
|
* used (through {@link InstanceCreator}); specified with {@code @JsonAdapter} on a class, and
|
|
* also specified with {@code @JsonAdapter} on a field of that class.
|
|
*
|
|
* <p><b>Important:</b> This situation is likely a rare corner case; the purpose of this test is
|
|
* to verify that Gson behaves reasonable, mainly that it does not cause a {@link
|
|
* StackOverflowError} due to infinite recursion. This test is not intended to dictate an expected
|
|
* behavior.
|
|
*/
|
|
@Test
|
|
public void testDelegating_SameFactoryInstance_OnClassAndField() {
|
|
WithDelegatingFactoryOnClassAndField.Factory factory =
|
|
new WithDelegatingFactoryOnClassAndField.Factory();
|
|
|
|
Gson gson =
|
|
new GsonBuilder()
|
|
.registerTypeAdapter(
|
|
String.class,
|
|
new TypeAdapter<String>() {
|
|
@Override
|
|
public String read(JsonReader in) throws IOException {
|
|
return in.nextString() + "-str";
|
|
}
|
|
|
|
@Override
|
|
public void write(JsonWriter out, String value) throws IOException {
|
|
out.value(value + "-str");
|
|
}
|
|
})
|
|
// Always provides same instance for factory
|
|
.registerTypeAdapter(
|
|
WithDelegatingFactoryOnClassAndField.Factory.class,
|
|
(InstanceCreator<?>) type -> factory)
|
|
.create();
|
|
|
|
// Because field type (`String`) differs from declaring class,
|
|
// JsonAdapterAnnotationTypeAdapterFactory does not confuse factories and this behaves as
|
|
// expected: Both the declaring class and the field each have `{"custom": ...}` and delegation
|
|
// for the field to the custom String adapter defined above works properly
|
|
WithDelegatingFactoryOnClassAndField deserialized =
|
|
gson.fromJson(
|
|
"{\"custom\":{\"f\":{\"custom\":\"de\"}}}", WithDelegatingFactoryOnClassAndField.class);
|
|
assertThat(deserialized.f).isEqualTo("de-str");
|
|
|
|
WithDelegatingFactoryOnClassAndField serialized =
|
|
new WithDelegatingFactoryOnClassAndField("se");
|
|
assertThat(gson.toJson(serialized)).isEqualTo("{\"custom\":{\"f\":{\"custom\":\"se-str\"}}}");
|
|
}
|
|
|
|
// Same factory class specified on class and one of its fields
|
|
@JsonAdapter(WithDelegatingFactoryOnClassAndField.Factory.class)
|
|
private static class WithDelegatingFactoryOnClassAndField {
|
|
// suppress Error Prone warning; should be clear that `Factory` refers to nested class
|
|
@SuppressWarnings("SameNameButDifferent")
|
|
@JsonAdapter(Factory.class)
|
|
String f;
|
|
|
|
WithDelegatingFactoryOnClassAndField(String f) {
|
|
this.f = f;
|
|
}
|
|
|
|
static class Factory implements TypeAdapterFactory {
|
|
@Override
|
|
public <T> TypeAdapter<T> create(Gson gson, TypeToken<T> type) {
|
|
TypeAdapter<T> delegate = gson.getDelegateAdapter(this, type);
|
|
|
|
return new TypeAdapter<T>() {
|
|
@Override
|
|
public T read(JsonReader in) throws IOException {
|
|
// Perform custom deserialization
|
|
in.beginObject();
|
|
assertThat(in.nextName()).isEqualTo("custom");
|
|
T t = delegate.read(in);
|
|
in.endObject();
|
|
|
|
return t;
|
|
}
|
|
|
|
@Override
|
|
public void write(JsonWriter out, T value) throws IOException {
|
|
// Perform custom serialization
|
|
out.beginObject();
|
|
out.name("custom");
|
|
delegate.write(out, value);
|
|
out.endObject();
|
|
}
|
|
};
|
|
}
|
|
}
|
|
}
|
|
|
|
/** Tests usage of {@link JsonSerializer} as {@link JsonAdapter} value */
|
|
@Test
|
|
public void testJsonSerializer() {
|
|
Gson gson = new Gson();
|
|
// Verify that delegate deserializer (reflection deserializer) is used
|
|
WithJsonSerializer deserialized = gson.fromJson("{\"f\":\"test\"}", WithJsonSerializer.class);
|
|
assertThat(deserialized.f).isEqualTo("test");
|
|
|
|
String json = gson.toJson(new WithJsonSerializer());
|
|
// Uses custom serializer which always returns `true`
|
|
assertThat(json).isEqualTo("true");
|
|
}
|
|
|
|
@JsonAdapter(WithJsonSerializer.Serializer.class)
|
|
private static class WithJsonSerializer {
|
|
String f = "";
|
|
|
|
static class Serializer implements JsonSerializer<WithJsonSerializer> {
|
|
@Override
|
|
public JsonElement serialize(
|
|
WithJsonSerializer src, Type typeOfSrc, JsonSerializationContext context) {
|
|
return new JsonPrimitive(true);
|
|
}
|
|
}
|
|
}
|
|
|
|
/** Tests usage of {@link JsonDeserializer} as {@link JsonAdapter} value */
|
|
@Test
|
|
public void testJsonDeserializer() {
|
|
Gson gson = new Gson();
|
|
WithJsonDeserializer deserialized =
|
|
gson.fromJson("{\"f\":\"test\"}", WithJsonDeserializer.class);
|
|
// Uses custom deserializer which always uses "123" as field value
|
|
assertThat(deserialized.f).isEqualTo("123");
|
|
|
|
// Verify that delegate serializer (reflection serializer) is used
|
|
String json = gson.toJson(new WithJsonDeserializer("abc"));
|
|
assertThat(json).isEqualTo("{\"f\":\"abc\"}");
|
|
}
|
|
|
|
@JsonAdapter(WithJsonDeserializer.Deserializer.class)
|
|
private static class WithJsonDeserializer {
|
|
String f;
|
|
|
|
WithJsonDeserializer(String f) {
|
|
this.f = f;
|
|
}
|
|
|
|
static class Deserializer implements JsonDeserializer<WithJsonDeserializer> {
|
|
@Override
|
|
public WithJsonDeserializer deserialize(
|
|
JsonElement json, Type typeOfT, JsonDeserializationContext context) {
|
|
return new WithJsonDeserializer("123");
|
|
}
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Tests creation of the adapter referenced by {@code @JsonAdapter} using an {@link
|
|
* InstanceCreator}.
|
|
*/
|
|
@Test
|
|
public void testAdapterCreatedByInstanceCreator() {
|
|
CreatedByInstanceCreator.Serializer serializer =
|
|
new CreatedByInstanceCreator.Serializer("custom");
|
|
Gson gson =
|
|
new GsonBuilder()
|
|
.registerTypeAdapter(
|
|
CreatedByInstanceCreator.Serializer.class, (InstanceCreator<?>) t -> serializer)
|
|
.create();
|
|
|
|
String json = gson.toJson(new CreatedByInstanceCreator());
|
|
assertThat(json).isEqualTo("\"custom\"");
|
|
}
|
|
|
|
@JsonAdapter(CreatedByInstanceCreator.Serializer.class)
|
|
private static class CreatedByInstanceCreator {
|
|
static class Serializer implements JsonSerializer<CreatedByInstanceCreator> {
|
|
private final String value;
|
|
|
|
@SuppressWarnings("unused")
|
|
public Serializer() {
|
|
throw new AssertionError("should not be called");
|
|
}
|
|
|
|
public Serializer(String value) {
|
|
this.value = value;
|
|
}
|
|
|
|
@Override
|
|
public JsonElement serialize(
|
|
CreatedByInstanceCreator src, Type typeOfSrc, JsonSerializationContext context) {
|
|
return new JsonPrimitive(value);
|
|
}
|
|
}
|
|
}
|
|
|
|
/** Tests creation of the adapter referenced by {@code @JsonAdapter} using JDK Unsafe. */
|
|
@Test
|
|
public void testAdapterCreatedByJdkUnsafe() {
|
|
String json = new Gson().toJson(new CreatedByJdkUnsafe());
|
|
assertThat(json).isEqualTo("false");
|
|
}
|
|
|
|
@JsonAdapter(CreatedByJdkUnsafe.Serializer.class)
|
|
private static class CreatedByJdkUnsafe {
|
|
static class Serializer implements JsonSerializer<CreatedByJdkUnsafe> {
|
|
// JDK Unsafe leaves this at default value `false`
|
|
private boolean wasInitialized = true;
|
|
|
|
// Explicit constructor with args to remove implicit no-args constructor
|
|
@SuppressWarnings("unused")
|
|
public Serializer(int i) {
|
|
throw new AssertionError("should not be called");
|
|
}
|
|
|
|
@Override
|
|
public JsonElement serialize(
|
|
CreatedByJdkUnsafe src, Type typeOfSrc, JsonSerializationContext context) {
|
|
return new JsonPrimitive(wasInitialized);
|
|
}
|
|
}
|
|
}
|
|
}
|