Back to Repositories

Testing StreamingTypeAdapter Implementation in google/gson

This test suite evaluates the StreamingTypeAdapter functionality in Google’s Gson library, focusing on JSON serialization and deserialization of complex objects using streaming APIs. The suite validates type adapter behavior for custom objects, collections, and nested data structures with various edge cases.

Test Coverage Overview

The test suite provides comprehensive coverage of StreamingTypeAdapter capabilities in Gson:
  • Basic object serialization/deserialization
  • Null handling and null safety
  • Custom type adapter implementation
  • Collection and map serialization
  • Array handling (1D and 2D)
  • Recursive object structures
  • JsonTree conversion

Implementation Analysis

The testing approach uses JUnit to validate the StreamingTypeAdapter functionality through detailed test cases. The implementation leverages TypeAdapter patterns with custom serialization logic, demonstrates proper error handling, and validates both primitive and complex object scenarios.

Key patterns include custom type adapter registration, null-safe adapter creation, and proper streaming API usage.

Technical Details

Testing Framework Components:
  • JUnit test framework
  • Gson TypeAdapter API
  • Custom test models (Truck, Person, Node)
  • JsonReader/JsonWriter for streaming operations
  • Truth assertion library for validation

Best Practices Demonstrated

The test suite exemplifies several testing best practices:
  • Comprehensive edge case coverage
  • Clear test method organization
  • Proper error validation
  • Isolated test scenarios
  • Reusable test fixtures
  • Readable assertion messages

google/gson

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

            
/*
 * Copyright (C) 2011 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.assertThrows;

import com.google.common.base.Splitter;
import com.google.gson.Gson;
import com.google.gson.GsonBuilder;
import com.google.gson.JsonArray;
import com.google.gson.JsonObject;
import com.google.gson.JsonPrimitive;
import com.google.gson.JsonSyntaxException;
import com.google.gson.TypeAdapter;
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.ArrayList;
import java.util.Arrays;
import java.util.Collections;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;
import org.junit.Test;

public final class StreamingTypeAdaptersTest {
  private Gson miniGson = new GsonBuilder().create();
  private TypeAdapter<Truck> truckAdapter = miniGson.getAdapter(Truck.class);
  private TypeAdapter<Map<String, Double>> mapAdapter =
      miniGson.getAdapter(new TypeToken<Map<String, Double>>() {});

  @Test
  public void testSerialize() {
    Truck truck = new Truck();
    truck.passengers = Arrays.asList(new Person("Jesse", 29), new Person("Jodie", 29));
    truck.horsePower = 300;

    assertThat(truckAdapter.toJson(truck).replace('\"', '\''))
        .isEqualTo(
            "{'horsePower':300.0,"
                + "'passengers':[{'age':29,'name':'Jesse'},{'age':29,'name':'Jodie'}]}");
  }

  @Test
  public void testDeserialize() throws IOException {
    String json =
        "{'horsePower':300.0,"
            + "'passengers':[{'age':29,'name':'Jesse'},{'age':29,'name':'Jodie'}]}";
    Truck truck = truckAdapter.fromJson(json.replace('\'', '\"'));
    assertThat(truck.horsePower).isEqualTo(300.0);
    assertThat(truck.passengers)
        .isEqualTo(Arrays.asList(new Person("Jesse", 29), new Person("Jodie", 29)));
  }

  @Test
  public void testSerializeNullField() {
    Truck truck = new Truck();
    truck.passengers = null;
    assertThat(truckAdapter.toJson(truck).replace('\"', '\''))
        .isEqualTo("{'horsePower':0.0,'passengers':null}");
  }

  @Test
  public void testDeserializeNullField() throws IOException {
    Truck truck = truckAdapter.fromJson("{'horsePower':0.0,'passengers':null}".replace('\'', '\"'));
    assertThat(truck.passengers).isNull();
  }

  @Test
  public void testSerializeNullObject() {
    Truck truck = new Truck();
    truck.passengers = Arrays.asList((Person) null);
    assertThat(truckAdapter.toJson(truck).replace('\"', '\''))
        .isEqualTo("{'horsePower':0.0,'passengers':[null]}");
  }

  @Test
  public void testDeserializeNullObject() throws IOException {
    Truck truck =
        truckAdapter.fromJson("{'horsePower':0.0,'passengers':[null]}".replace('\'', '\"'));
    assertThat(truck.passengers).isEqualTo(Arrays.asList((Person) null));
  }

  @Test
  public void testSerializeWithCustomTypeAdapter() {
    usePersonNameAdapter();
    Truck truck = new Truck();
    truck.passengers = Arrays.asList(new Person("Jesse", 29), new Person("Jodie", 29));
    assertThat(truckAdapter.toJson(truck).replace('\"', '\''))
        .isEqualTo("{'horsePower':0.0,'passengers':['Jesse','Jodie']}");
  }

  @Test
  public void testDeserializeWithCustomTypeAdapter() throws IOException {
    usePersonNameAdapter();
    Truck truck =
        truckAdapter.fromJson(
            "{'horsePower':0.0,'passengers':['Jesse','Jodie']}".replace('\'', '\"'));
    assertThat(truck.passengers)
        .isEqualTo(Arrays.asList(new Person("Jesse", -1), new Person("Jodie", -1)));
  }

  private void usePersonNameAdapter() {
    TypeAdapter<Person> personNameAdapter =
        new TypeAdapter<>() {
          @Override
          public Person read(JsonReader in) throws IOException {
            String name = in.nextString();
            return new Person(name, -1);
          }

          @Override
          public void write(JsonWriter out, Person value) throws IOException {
            out.value(value.name);
          }
        };
    miniGson = new GsonBuilder().registerTypeAdapter(Person.class, personNameAdapter).create();
    truckAdapter = miniGson.getAdapter(Truck.class);
  }

  @Test
  public void testSerializeMap() {
    Map<String, Double> map = new LinkedHashMap<>();
    map.put("a", 5.0);
    map.put("b", 10.0);
    assertThat(mapAdapter.toJson(map).replace('"', '\'')).isEqualTo("{'a':5.0,'b':10.0}");
  }

  @Test
  public void testDeserializeMap() throws IOException {
    Map<String, Double> map = new LinkedHashMap<>();
    map.put("a", 5.0);
    map.put("b", 10.0);
    assertThat(mapAdapter.fromJson("{'a':5.0,'b':10.0}".replace('\'', '\"'))).isEqualTo(map);
  }

  @Test
  public void testSerialize1dArray() {
    TypeAdapter<double[]> arrayAdapter = miniGson.getAdapter(new TypeToken<double[]>() {});
    assertThat(arrayAdapter.toJson(new double[] {1.0, 2.0, 3.0})).isEqualTo("[1.0,2.0,3.0]");
  }

  @Test
  public void testDeserialize1dArray() throws IOException {
    TypeAdapter<double[]> arrayAdapter = miniGson.getAdapter(new TypeToken<double[]>() {});
    double[] array = arrayAdapter.fromJson("[1.0,2.0,3.0]");
    assertThat(array).isEqualTo(new double[] {1.0, 2.0, 3.0});
  }

  @Test
  public void testSerialize2dArray() {
    TypeAdapter<double[][]> arrayAdapter = miniGson.getAdapter(new TypeToken<double[][]>() {});
    double[][] array = {{1.0, 2.0}, {3.0}};
    assertThat(arrayAdapter.toJson(array)).isEqualTo("[[1.0,2.0],[3.0]]");
  }

  @Test
  public void testDeserialize2dArray() throws IOException {
    TypeAdapter<double[][]> arrayAdapter = miniGson.getAdapter(new TypeToken<double[][]>() {});
    double[][] array = arrayAdapter.fromJson("[[1.0,2.0],[3.0]]");
    double[][] expected = {{1.0, 2.0}, {3.0}};
    assertThat(array).isEqualTo(expected);
  }

  @Test
  public void testNullSafe() {
    TypeAdapter<Person> typeAdapter =
        new TypeAdapter<>() {
          @Override
          public Person read(JsonReader in) throws IOException {
            List<String> values = Splitter.on(',').splitToList(in.nextString());
            return new Person(values.get(0), Integer.parseInt(values.get(1)));
          }

          @Override
          public void write(JsonWriter out, Person person) throws IOException {
            out.value(person.name + "," + person.age);
          }
        };

    Gson gson = new GsonBuilder().registerTypeAdapter(Person.class, typeAdapter).create();
    Truck truck = new Truck();
    truck.horsePower = 1.0D;
    truck.passengers = new ArrayList<>();
    truck.passengers.add(null);
    truck.passengers.add(new Person("jesse", 30));
    assertThrows(NullPointerException.class, () -> gson.toJson(truck, Truck.class));

    String json = "{horsePower:1.0,passengers:[null,'jesse,30']}";
    var e = assertThrows(JsonSyntaxException.class, () -> gson.fromJson(json, Truck.class));
    assertThat(e)
        .hasMessageThat()
        .isEqualTo(
            "java.lang.IllegalStateException: Expected a string but was NULL at line 1 column 33"
                + " path $.passengers[0]\n"
                + "See https://github.com/google/gson/blob/main/Troubleshooting.md#adapter-not-null-safe");

    Gson gson2 =
        new GsonBuilder().registerTypeAdapter(Person.class, typeAdapter.nullSafe()).create();
    assertThat(gson2.toJson(truck, Truck.class))
        .isEqualTo("{\"horsePower\":1.0,\"passengers\":[null,\"jesse,30\"]}");
    Truck deserialized = gson2.fromJson(json, Truck.class);
    assertThat(deserialized.horsePower).isEqualTo(1.0D);
    assertThat(deserialized.passengers.get(0)).isNull();
    assertThat(deserialized.passengers.get(1).name).isEqualTo("jesse");
  }

  @Test
  public void testSerializeRecursive() {
    TypeAdapter<Node> nodeAdapter = miniGson.getAdapter(Node.class);
    Node root = new Node("root");
    root.left = new Node("left");
    root.right = new Node("right");
    assertThat(nodeAdapter.toJson(root).replace('"', '\''))
        .isEqualTo(
            "{'label':'root',"
                + "'left':{'label':'left','left':null,'right':null},"
                + "'right':{'label':'right','left':null,'right':null}}");
  }

  @Test
  public void testFromJsonTree() {
    JsonObject truckObject = new JsonObject();
    truckObject.add("horsePower", new JsonPrimitive(300));
    JsonArray passengersArray = new JsonArray();
    JsonObject jesseObject = new JsonObject();
    jesseObject.add("age", new JsonPrimitive(30));
    jesseObject.add("name", new JsonPrimitive("Jesse"));
    passengersArray.add(jesseObject);
    truckObject.add("passengers", passengersArray);

    Truck truck = truckAdapter.fromJsonTree(truckObject);
    assertThat(truck.horsePower).isEqualTo(300.0);
    assertThat(truck.passengers).isEqualTo(Arrays.asList(new Person("Jesse", 30)));
  }

  static class Truck {
    double horsePower;
    List<Person> passengers = Collections.emptyList();
  }

  static class Person {
    int age;
    String name;

    Person(String name, int age) {
      this.name = name;
      this.age = age;
    }

    @Override
    public boolean equals(Object o) {
      return o instanceof Person && ((Person) o).name.equals(name) && ((Person) o).age == age;
    }

    @Override
    public int hashCode() {
      return name.hashCode() ^ age;
    }
  }

  static class Node {
    String label;
    Node left;
    Node right;

    Node(String label) {
      this.label = label;
    }
  }
}