Testing Protocol Buffer Annotation Processing in google/gson
This test suite validates Protocol Buffer functionality with annotations in the GSON library, focusing on serialization and deserialization of protobuf messages with custom field names and enum values. The tests verify proper handling of annotated proto messages, enum serialization formats, and various edge cases.
Test Coverage Overview
Implementation Analysis
Technical Details
Best Practices Demonstrated
google/gson
proto/src/test/java/com/google/gson/protobuf/functional/ProtosWithAnnotationsTest.java
/*
* Copyright (C) 2010 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.protobuf.functional;
import static com.google.common.truth.Truth.assertThat;
import static org.junit.Assert.assertThrows;
import com.google.common.base.CaseFormat;
import com.google.gson.Gson;
import com.google.gson.GsonBuilder;
import com.google.gson.JsonParseException;
import com.google.gson.protobuf.ProtoTypeAdapter;
import com.google.gson.protobuf.ProtoTypeAdapter.EnumSerialization;
import com.google.gson.protobuf.generated.Annotations;
import com.google.gson.protobuf.generated.Bag.OuterMessage;
import com.google.gson.protobuf.generated.Bag.ProtoWithAnnotations;
import com.google.gson.protobuf.generated.Bag.ProtoWithAnnotations.InnerMessage;
import com.google.protobuf.GeneratedMessage;
import org.junit.Before;
import org.junit.Test;
/**
* Functional tests for protocol buffers using annotations for field names and enum values.
*
* @author Emmanuel Cron
*/
public class ProtosWithAnnotationsTest {
private Gson gson;
private Gson gsonWithEnumNumbers;
private Gson gsonWithLowerHyphen;
@Before
public void setUp() throws Exception {
ProtoTypeAdapter.Builder protoTypeAdapter =
ProtoTypeAdapter.newBuilder()
.setEnumSerialization(EnumSerialization.NAME)
.addSerializedNameExtension(Annotations.serializedName)
.addSerializedEnumValueExtension(Annotations.serializedValue);
gson =
new GsonBuilder()
.registerTypeHierarchyAdapter(GeneratedMessage.class, protoTypeAdapter.build())
.create();
gsonWithEnumNumbers =
new GsonBuilder()
.registerTypeHierarchyAdapter(
GeneratedMessage.class,
protoTypeAdapter.setEnumSerialization(EnumSerialization.NUMBER).build())
.create();
gsonWithLowerHyphen =
new GsonBuilder()
.registerTypeHierarchyAdapter(
GeneratedMessage.class,
protoTypeAdapter
.setFieldNameSerializationFormat(
CaseFormat.LOWER_UNDERSCORE, CaseFormat.LOWER_HYPHEN)
.build())
.create();
}
@Test
public void testProtoWithAnnotations_deserialize() {
String json =
String.format(
"{ %n"
+ " \"id\":\"41e5e7fd6065d101b97018a465ffff01\",%n"
+ " \"expiration_date\":{ %n"
+ " \"month\":\"12\",%n"
+ " \"year\":\"2017\",%n"
+ " \"timeStamp\":\"9864653135687\",%n"
+ " \"countryCode5f55\":\"en_US\"%n"
+ " },%n"
// Don't define innerMessage1
+ " \"innerMessage2\":{ %n"
// Set a number as a string; it should work
+ " \"nIdCt\":\"98798465\",%n"
+ " \"content\":\"text/plain\",%n"
+ " \"$binary_data$\":[ %n"
+ " { %n"
+ " \"data\":\"OFIN8e9fhwoeh8((⁹8efywoih\",%n"
// Don't define width
+ " \"height\":665%n"
+ " },%n"
+ " { %n"
// Define as an int; it should work
+ " \"data\":65,%n"
+ " \"width\":-56684%n"
// Don't define height
+ " }%n"
+ " ]%n"
+ " },%n"
// Define a bunch of non recognizable data
+ " \"non_existing\":\"foobar\",%n"
+ " \"stillNot\":{ %n"
+ " \"bunch\":\"of_useless data\"%n"
+ " }%n"
+ "}");
ProtoWithAnnotations proto = gson.fromJson(json, ProtoWithAnnotations.class);
assertThat(proto.getId()).isEqualTo("41e5e7fd6065d101b97018a465ffff01");
assertThat(proto.getOuterMessage())
.isEqualTo(
OuterMessage.newBuilder()
.setMonth(12)
.setYear(2017)
.setLongTimestamp(9864653135687L)
.setCountryCode5F55("en_US")
.build());
assertThat(proto.hasInnerMessage1()).isFalse();
assertThat(proto.getInnerMessage2())
.isEqualTo(
InnerMessage.newBuilder()
.setNIdCt(98798465)
.setContent(InnerMessage.Type.TEXT)
.addData(
InnerMessage.Data.newBuilder()
.setData("OFIN8e9fhwoeh8((⁹8efywoih")
.setHeight(665))
.addData(InnerMessage.Data.newBuilder().setData("65").setWidth(-56684))
.build());
String rebuilt = gson.toJson(proto);
assertThat(rebuilt)
.isEqualTo(
"{"
+ "\"id\":\"41e5e7fd6065d101b97018a465ffff01\","
+ "\"expiration_date\":{"
+ "\"month\":12,"
+ "\"year\":2017,"
+ "\"timeStamp\":9864653135687,"
+ "\"countryCode5f55\":\"en_US\""
+ "},"
+ "\"innerMessage2\":{"
+ "\"nIdCt\":98798465,"
+ "\"content\":\"text/plain\","
+ "\"$binary_data$\":["
+ "{"
+ "\"data\":\"OFIN8e9fhwoeh8((⁹8efywoih\","
+ "\"height\":665"
+ "},"
+ "{"
+ "\"data\":\"65\","
+ "\"width\":-56684"
+ "}]}}");
}
@Test
public void testProtoWithAnnotations_deserializeUnknownEnumValue() {
String json = String.format("{ %n" + " \"content\":\"UNKNOWN\"%n" + "}");
InnerMessage proto = gson.fromJson(json, InnerMessage.class);
assertThat(proto.getContent()).isEqualTo(InnerMessage.Type.UNKNOWN);
}
@Test
public void testProtoWithAnnotations_deserializeUnrecognizedEnumValue() {
String json = String.format("{ %n" + " \"content\":\"UNRECOGNIZED\"%n" + "}");
var e = assertThrows(JsonParseException.class, () -> gson.fromJson(json, InnerMessage.class));
assertThat(e).hasMessageThat().isEqualTo("Error while parsing proto");
assertThat(e).hasCauseThat().hasMessageThat().isEqualTo("Unrecognized enum name: UNRECOGNIZED");
}
@Test
public void testProtoWithAnnotations_deserializeWithEnumNumbers() {
String json = String.format("{ %n" + " \"content\":\"0\"%n" + "}");
InnerMessage proto = gsonWithEnumNumbers.fromJson(json, InnerMessage.class);
assertThat(proto.getContent()).isEqualTo(InnerMessage.Type.UNKNOWN);
String rebuilt = gsonWithEnumNumbers.toJson(proto);
assertThat(rebuilt).isEqualTo("{\"content\":0}");
json = String.format("{ %n" + " \"content\":\"2\"%n" + "}");
proto = gsonWithEnumNumbers.fromJson(json, InnerMessage.class);
assertThat(proto.getContent()).isEqualTo(InnerMessage.Type.IMAGE);
rebuilt = gsonWithEnumNumbers.toJson(proto);
assertThat(rebuilt).isEqualTo("{\"content\":2}");
}
@Test
public void testProtoWithAnnotations_deserializeUnrecognizedEnumNumber() {
String json = String.format("{ %n" + " \"content\":\"99\"%n" + "}");
var e =
assertThrows(
JsonParseException.class, () -> gsonWithEnumNumbers.fromJson(json, InnerMessage.class));
assertThat(e).hasMessageThat().isEqualTo("Error while parsing proto");
assertThat(e).hasCauseThat().hasMessageThat().isEqualTo("Unrecognized enum value: 99");
}
@Test
public void testProtoWithAnnotations_serialize() {
ProtoWithAnnotations proto =
ProtoWithAnnotations.newBuilder()
.setId("09f3j20839h032y0329hf30932h0nffn")
.setOuterMessage(
OuterMessage.newBuilder()
.setMonth(14)
.setYear(6650)
.setLongTimestamp(468406876880768L))
.setInnerMessage1(
InnerMessage.newBuilder()
.setNIdCt(12)
.setContent(InnerMessage.Type.IMAGE)
.addData(InnerMessage.Data.newBuilder().setData("data$$").setWidth(200))
.addData(InnerMessage.Data.newBuilder().setHeight(56)))
.build();
String json = gsonWithLowerHyphen.toJson(proto);
assertThat(json)
.isEqualTo(
"{\"id\":\"09f3j20839h032y0329hf30932h0nffn\","
+ "\"expiration_date\":{"
+ "\"month\":14,"
+ "\"year\":6650,"
+ "\"timeStamp\":468406876880768"
+ "},"
// This field should be using hyphens
+ "\"inner-message-1\":{"
+ "\"n--id-ct\":12,"
+ "\"content\":2,"
+ "\"$binary_data$\":["
+ "{"
+ "\"data\":\"data$$\","
+ "\"width\":200"
+ "},"
+ "{"
+ "\"height\":56"
+ "}]"
+ "}"
+ "}");
ProtoWithAnnotations rebuilt = gsonWithLowerHyphen.fromJson(json, ProtoWithAnnotations.class);
assertThat(rebuilt).isEqualTo(proto);
}
}