Back to Repositories

Testing Custom Type Adapter Implementation in Google Gson

This test suite validates custom type adapters functionality in the Google Gson library, focusing on serialization and deserialization of custom objects with specialized handling. It covers both primitive and complex type conversions while demonstrating proper integration with Gson’s type adapter system.

Test Coverage Overview

The test suite provides comprehensive coverage of custom type adapter implementations, including:

  • Serialization and deserialization of primitive types
  • Custom handling of nested objects and collections
  • Type hierarchy adapter testing
  • Null value handling scenarios
  • Collection and Map element conversion

Implementation Analysis

The testing approach utilizes JUnit framework with custom type adapters implementing JsonSerializer and JsonDeserializer interfaces. The tests demonstrate proper usage of GsonBuilder registration methods and verify correct behavior of type conversion logic across different data structures.

Technical Details

  • Testing Framework: JUnit
  • Key Classes: GsonBuilder, JsonSerializer, JsonDeserializer
  • Test Utilities: Truth assertion library
  • Custom Components: StringHolder, DataHolder type adapters

Best Practices Demonstrated

The test suite exemplifies several testing best practices:

  • Thorough edge case coverage including null handling
  • Clear test method naming conventions
  • Proper test isolation and setup
  • Comprehensive assertion validation
  • Modular test class organization

google/gson

gson/src/test/java/com/google/gson/functional/CustomTypeAdaptersTest.java

            
/*
 * Copyright (C) 2008 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 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.JsonObject;
import com.google.gson.JsonParseException;
import com.google.gson.JsonPrimitive;
import com.google.gson.JsonSerializationContext;
import com.google.gson.JsonSerializer;
import com.google.gson.common.TestTypes.BagOfPrimitives;
import com.google.gson.common.TestTypes.ClassWithCustomTypeConverter;
import com.google.gson.reflect.TypeToken;
import java.lang.reflect.Type;
import java.util.Date;
import java.util.HashMap;
import java.util.HashSet;
import java.util.List;
import java.util.Map;
import java.util.Set;
import org.junit.Before;
import org.junit.Ignore;
import org.junit.Test;

/**
 * Functional tests for the support of custom serializer and deserializers.
 *
 * @author Inderjeet Singh
 * @author Joel Leitch
 */
public class CustomTypeAdaptersTest {
  private GsonBuilder builder;

  @Before
  public void setUp() throws Exception {
    builder = new GsonBuilder();
  }

  @Test
  public void testCustomSerializers() {
    Gson gson =
        builder
            .registerTypeAdapter(
                ClassWithCustomTypeConverter.class,
                (JsonSerializer<ClassWithCustomTypeConverter>)
                    (src, typeOfSrc, context) -> {
                      JsonObject json = new JsonObject();
                      json.addProperty("bag", 5);
                      json.addProperty("value", 25);
                      return json;
                    })
            .create();
    ClassWithCustomTypeConverter target = new ClassWithCustomTypeConverter();
    assertThat(gson.toJson(target)).isEqualTo("{\"bag\":5,\"value\":25}");
  }

  @Test
  public void testCustomDeserializers() {
    Gson gson =
        new GsonBuilder()
            .registerTypeAdapter(
                ClassWithCustomTypeConverter.class,
                (JsonDeserializer<ClassWithCustomTypeConverter>)
                    (json, typeOfT, context) -> {
                      JsonObject jsonObject = json.getAsJsonObject();
                      int value = jsonObject.get("bag").getAsInt();
                      return new ClassWithCustomTypeConverter(
                          new BagOfPrimitives(value, value, false, ""), value);
                    })
            .create();
    String json = "{\"bag\":5,\"value\":25}";
    ClassWithCustomTypeConverter target = gson.fromJson(json, ClassWithCustomTypeConverter.class);
    assertThat(target.getBag().getIntValue()).isEqualTo(5);
  }

  @Test
  @Ignore
  public void disable_testCustomSerializersOfSelf() {
    Gson gson = createGsonObjectWithFooTypeAdapter();
    Gson basicGson = new Gson();
    Foo newFooObject = new Foo(1, 2L);
    String jsonFromCustomSerializer = gson.toJson(newFooObject);
    String jsonFromGson = basicGson.toJson(newFooObject);

    assertThat(jsonFromCustomSerializer).isEqualTo(jsonFromGson);
  }

  @Test
  @Ignore
  public void disable_testCustomDeserializersOfSelf() {
    Gson gson = createGsonObjectWithFooTypeAdapter();
    Gson basicGson = new Gson();
    Foo expectedFoo = new Foo(1, 2L);
    String json = basicGson.toJson(expectedFoo);
    Foo newFooObject = gson.fromJson(json, Foo.class);

    assertThat(newFooObject.key).isEqualTo(expectedFoo.key);
    assertThat(newFooObject.value).isEqualTo(expectedFoo.value);
  }

  @Test
  public void testCustomNestedSerializers() {
    Gson gson =
        new GsonBuilder()
            .registerTypeAdapter(
                BagOfPrimitives.class,
                (JsonSerializer<BagOfPrimitives>) (src, typeOfSrc, context) -> new JsonPrimitive(6))
            .create();
    ClassWithCustomTypeConverter target = new ClassWithCustomTypeConverter();
    assertThat(gson.toJson(target)).isEqualTo("{\"bag\":6,\"value\":10}");
  }

  @Test
  public void testCustomNestedDeserializers() {
    Gson gson =
        new GsonBuilder()
            .registerTypeAdapter(
                BagOfPrimitives.class,
                (JsonDeserializer<BagOfPrimitives>)
                    (json, typeOfT, context) -> {
                      int value = json.getAsInt();
                      return new BagOfPrimitives(value, value, false, "");
                    })
            .create();
    String json = "{\"bag\":7,\"value\":25}";
    ClassWithCustomTypeConverter target = gson.fromJson(json, ClassWithCustomTypeConverter.class);
    assertThat(target.getBag().getIntValue()).isEqualTo(7);
  }

  @Test
  public void testCustomTypeAdapterDoesNotAppliesToSubClasses() {
    Gson gson =
        new GsonBuilder()
            .registerTypeAdapter(
                Base.class,
                (JsonSerializer<Base>)
                    (src, typeOfSrc, context) -> {
                      JsonObject json = new JsonObject();
                      json.addProperty("value", src.baseValue);
                      return json;
                    })
            .create();
    Base b = new Base();
    String json = gson.toJson(b);
    assertThat(json).contains("value");
    b = new Derived();
    json = gson.toJson(b);
    assertThat(json).contains("derivedValue");
  }

  @Test
  public void testCustomTypeAdapterAppliesToSubClassesSerializedAsBaseClass() {
    Gson gson =
        new GsonBuilder()
            .registerTypeAdapter(
                Base.class,
                (JsonSerializer<Base>)
                    (src, typeOfSrc, context) -> {
                      JsonObject json = new JsonObject();
                      json.addProperty("value", src.baseValue);
                      return json;
                    })
            .create();
    Base b = new Base();
    String json = gson.toJson(b);
    assertThat(json).contains("value");
    b = new Derived();
    json = gson.toJson(b, Base.class);
    assertThat(json).contains("value");
    assertThat(json).doesNotContain("derivedValue");
  }

  private static class Base {
    int baseValue = 2;
  }

  private static class Derived extends Base {
    @SuppressWarnings("unused")
    int derivedValue = 3;
  }

  private Gson createGsonObjectWithFooTypeAdapter() {
    return new GsonBuilder().registerTypeAdapter(Foo.class, new FooTypeAdapter()).create();
  }

  public static class Foo {
    private final int key;
    private final long value;

    public Foo() {
      this(0, 0L);
    }

    public Foo(int key, long value) {
      this.key = key;
      this.value = value;
    }
  }

  public static final class FooTypeAdapter implements JsonSerializer<Foo>, JsonDeserializer<Foo> {
    @Override
    public Foo deserialize(JsonElement json, Type typeOfT, JsonDeserializationContext context)
        throws JsonParseException {
      return context.deserialize(json, typeOfT);
    }

    @Override
    public JsonElement serialize(Foo src, Type typeOfSrc, JsonSerializationContext context) {
      return context.serialize(src, typeOfSrc);
    }
  }

  @Test
  public void testCustomSerializerInvokedForPrimitives() {
    Gson gson =
        new GsonBuilder()
            .registerTypeAdapter(
                boolean.class,
                (JsonSerializer<Boolean>)
                    (value, type, context) -> new JsonPrimitive(value ? 1 : 0))
            .create();
    assertThat(gson.toJson(true, boolean.class)).isEqualTo("1");
    assertThat(gson.toJson(true, Boolean.class)).isEqualTo("true");
  }

  @Test
  public void testCustomDeserializerInvokedForPrimitives() {
    Gson gson =
        new GsonBuilder()
            .registerTypeAdapter(
                boolean.class,
                (JsonDeserializer<Boolean>) (json, type, context) -> json.getAsInt() != 0)
            .create();
    assertThat(gson.fromJson("1", boolean.class)).isEqualTo(Boolean.TRUE);
    assertThat(gson.fromJson("true", Boolean.class)).isEqualTo(Boolean.TRUE);
  }

  @Test
  public void testCustomByteArraySerializer() {
    Gson gson =
        new GsonBuilder()
            .registerTypeAdapter(
                byte[].class,
                (JsonSerializer<byte[]>)
                    (src, typeOfSrc, context) -> {
                      StringBuilder sb = new StringBuilder(src.length);
                      for (byte b : src) {
                        sb.append(b);
                      }
                      return new JsonPrimitive(sb.toString());
                    })
            .create();
    byte[] data = {0, 1, 2, 3, 4, 5, 6, 7, 8, 9};
    String json = gson.toJson(data);
    assertThat(json).isEqualTo("\"0123456789\"");
  }

  @Test
  public void testCustomByteArrayDeserializerAndInstanceCreator() {
    GsonBuilder gsonBuilder =
        new GsonBuilder()
            .registerTypeAdapter(
                byte[].class,
                (JsonDeserializer<byte[]>)
                    (json, typeOfT, context) -> {
                      String str = json.getAsString();
                      byte[] data = new byte[str.length()];
                      for (int i = 0; i < data.length; ++i) {
                        data[i] = Byte.parseByte("" + str.charAt(i));
                      }
                      return data;
                    });
    Gson gson = gsonBuilder.create();
    String json = "'0123456789'";
    byte[] actual = gson.fromJson(json, byte[].class);
    byte[] expected = {0, 1, 2, 3, 4, 5, 6, 7, 8, 9};
    for (int i = 0; i < actual.length; ++i) {
      assertThat(actual[i]).isEqualTo(expected[i]);
    }
  }

  private static final class StringHolder {
    String part1;
    String part2;

    public StringHolder(String string) {
      List<String> parts = Splitter.on(':').splitToList(string);
      part1 = parts.get(0);
      part2 = parts.get(1);
    }

    public StringHolder(String part1, String part2) {
      this.part1 = part1;
      this.part2 = part2;
    }
  }

  private static class StringHolderTypeAdapter
      implements JsonSerializer<StringHolder>,
          JsonDeserializer<StringHolder>,
          InstanceCreator<StringHolder> {

    @Override
    public StringHolder createInstance(Type type) {
      // Fill up with objects that will be thrown away
      return new StringHolder("unknown:thing");
    }

    @Override
    public StringHolder deserialize(
        JsonElement src, Type type, JsonDeserializationContext context) {
      return new StringHolder(src.getAsString());
    }

    @Override
    public JsonElement serialize(
        StringHolder src, Type typeOfSrc, JsonSerializationContext context) {
      String contents = src.part1 + ':' + src.part2;
      return new JsonPrimitive(contents);
    }
  }

  // Test created from Issue 70
  @Test
  public void testCustomAdapterInvokedForCollectionElementSerializationWithType() {
    Gson gson =
        new GsonBuilder()
            .registerTypeAdapter(StringHolder.class, new StringHolderTypeAdapter())
            .create();
    Type setType = new TypeToken<Set<StringHolder>>() {}.getType();
    StringHolder holder = new StringHolder("Jacob", "Tomaw");
    Set<StringHolder> setOfHolders = new HashSet<>();
    setOfHolders.add(holder);
    String json = gson.toJson(setOfHolders, setType);
    assertThat(json).contains("Jacob:Tomaw");
  }

  // Test created from Issue 70
  @Test
  public void testCustomAdapterInvokedForCollectionElementSerialization() {
    Gson gson =
        new GsonBuilder()
            .registerTypeAdapter(StringHolder.class, new StringHolderTypeAdapter())
            .create();
    StringHolder holder = new StringHolder("Jacob", "Tomaw");
    Set<StringHolder> setOfHolders = new HashSet<>();
    setOfHolders.add(holder);
    String json = gson.toJson(setOfHolders);
    assertThat(json).contains("Jacob:Tomaw");
  }

  // Test created from Issue 70
  @Test
  public void testCustomAdapterInvokedForCollectionElementDeserialization() {
    Gson gson =
        new GsonBuilder()
            .registerTypeAdapter(StringHolder.class, new StringHolderTypeAdapter())
            .create();
    Type setType = new TypeToken<Set<StringHolder>>() {}.getType();
    Set<StringHolder> setOfHolders = gson.fromJson("['Jacob:Tomaw']", setType);
    assertThat(setOfHolders.size()).isEqualTo(1);
    StringHolder foo = setOfHolders.iterator().next();
    assertThat(foo.part1).isEqualTo("Jacob");
    assertThat(foo.part2).isEqualTo("Tomaw");
  }

  // Test created from Issue 70
  @Test
  public void testCustomAdapterInvokedForMapElementSerializationWithType() {
    Gson gson =
        new GsonBuilder()
            .registerTypeAdapter(StringHolder.class, new StringHolderTypeAdapter())
            .create();
    Type mapType = new TypeToken<Map<String, StringHolder>>() {}.getType();
    StringHolder holder = new StringHolder("Jacob", "Tomaw");
    Map<String, StringHolder> mapOfHolders = new HashMap<>();
    mapOfHolders.put("foo", holder);
    String json = gson.toJson(mapOfHolders, mapType);
    assertThat(json).contains("\"foo\":\"Jacob:Tomaw\"");
  }

  // Test created from Issue 70
  @Test
  public void testCustomAdapterInvokedForMapElementSerialization() {
    Gson gson =
        new GsonBuilder()
            .registerTypeAdapter(StringHolder.class, new StringHolderTypeAdapter())
            .create();
    StringHolder holder = new StringHolder("Jacob", "Tomaw");
    Map<String, StringHolder> mapOfHolders = new HashMap<>();
    mapOfHolders.put("foo", holder);
    String json = gson.toJson(mapOfHolders);
    assertThat(json).contains("\"foo\":\"Jacob:Tomaw\"");
  }

  // Test created from Issue 70
  @Test
  public void testCustomAdapterInvokedForMapElementDeserialization() {
    Gson gson =
        new GsonBuilder()
            .registerTypeAdapter(StringHolder.class, new StringHolderTypeAdapter())
            .create();
    Type mapType = new TypeToken<Map<String, StringHolder>>() {}.getType();
    Map<String, StringHolder> mapOfFoo = gson.fromJson("{'foo':'Jacob:Tomaw'}", mapType);
    assertThat(mapOfFoo.size()).isEqualTo(1);
    StringHolder foo = mapOfFoo.get("foo");
    assertThat(foo.part1).isEqualTo("Jacob");
    assertThat(foo.part2).isEqualTo("Tomaw");
  }

  @Test
  public void testEnsureCustomSerializerNotInvokedForNullValues() {
    Gson gson =
        new GsonBuilder()
            .registerTypeAdapter(DataHolder.class, new DataHolderSerializer())
            .create();
    DataHolderWrapper target = new DataHolderWrapper(new DataHolder("abc"));
    String json = gson.toJson(target);
    assertThat(json).isEqualTo("{\"wrappedData\":{\"myData\":\"abc\"}}");
  }

  @Test
  public void testEnsureCustomDeserializerNotInvokedForNullValues() {
    Gson gson =
        new GsonBuilder()
            .registerTypeAdapter(DataHolder.class, new DataHolderDeserializer())
            .create();
    String json = "{wrappedData:null}";
    DataHolderWrapper actual = gson.fromJson(json, DataHolderWrapper.class);
    assertThat(actual.wrappedData).isNull();
  }

  // Test created from Issue 352
  @Test
  @SuppressWarnings({"JavaUtilDate", "UndefinedEquals"})
  public void testRegisterHierarchyAdapterForDate() {
    Gson gson =
        new GsonBuilder().registerTypeHierarchyAdapter(Date.class, new DateTypeAdapter()).create();
    assertThat(gson.toJson(new Date(0))).isEqualTo("0");
    assertThat(gson.toJson(new java.sql.Date(0))).isEqualTo("0");
    assertThat(gson.fromJson("0", Date.class)).isEqualTo(new Date(0));
    assertThat(gson.fromJson("0", java.sql.Date.class)).isEqualTo(new java.sql.Date(0));
  }

  private static class DataHolder {
    final String data;

    public DataHolder(String data) {
      this.data = data;
    }
  }

  private static class DataHolderWrapper {
    final DataHolder wrappedData;

    public DataHolderWrapper(DataHolder data) {
      this.wrappedData = data;
    }
  }

  private static class DataHolderSerializer implements JsonSerializer<DataHolder> {
    @Override
    public JsonElement serialize(DataHolder src, Type typeOfSrc, JsonSerializationContext context) {
      JsonObject obj = new JsonObject();
      obj.addProperty("myData", src.data);
      return obj;
    }
  }

  private static class DataHolderDeserializer implements JsonDeserializer<DataHolder> {
    @Override
    public DataHolder deserialize(
        JsonElement json, Type typeOfT, JsonDeserializationContext context)
        throws JsonParseException {
      JsonObject jsonObj = json.getAsJsonObject();
      JsonElement jsonElement = jsonObj.get("data");
      if (jsonElement == null || jsonElement.isJsonNull()) {
        return new DataHolder(null);
      }
      return new DataHolder(jsonElement.getAsString());
    }
  }

  @SuppressWarnings("JavaUtilDate")
  private static class DateTypeAdapter implements JsonSerializer<Date>, JsonDeserializer<Date> {
    @Override
    public Date deserialize(JsonElement json, Type typeOfT, JsonDeserializationContext context) {
      return typeOfT == Date.class
          ? new Date(json.getAsLong())
          : new java.sql.Date(json.getAsLong());
    }

    @Override
    public JsonElement serialize(Date src, Type typeOfSrc, JsonSerializationContext context) {
      return new JsonPrimitive(src.getTime());
    }
  }
}