Improve versioning support documentation and validate version (#2214)

This commit is contained in:
Marcono1234 2022-10-03 01:38:43 +02:00 committed by GitHub
parent 28609089fa
commit 796193d032
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
7 changed files with 180 additions and 64 deletions

View File

@ -28,6 +28,8 @@ import static com.google.gson.Gson.DEFAULT_SERIALIZE_NULLS;
import static com.google.gson.Gson.DEFAULT_SPECIALIZE_FLOAT_VALUES; import static com.google.gson.Gson.DEFAULT_SPECIALIZE_FLOAT_VALUES;
import static com.google.gson.Gson.DEFAULT_USE_JDK_UNSAFE; import static com.google.gson.Gson.DEFAULT_USE_JDK_UNSAFE;
import com.google.gson.annotations.Since;
import com.google.gson.annotations.Until;
import com.google.gson.internal.$Gson$Preconditions; import com.google.gson.internal.$Gson$Preconditions;
import com.google.gson.internal.Excluder; import com.google.gson.internal.Excluder;
import com.google.gson.internal.bind.DefaultDateTypeAdapter; import com.google.gson.internal.bind.DefaultDateTypeAdapter;
@ -143,14 +145,25 @@ public final class GsonBuilder {
} }
/** /**
* Configures Gson to enable versioning support. * Configures Gson to enable versioning support. Versioning support works based on the
* annotation types {@link Since} and {@link Until}. It allows including or excluding fields
* and classes based on the specified version. See the documentation of these annotation
* types for more information.
* *
* @param ignoreVersionsAfter any field or type marked with a version higher than this value * <p>By default versioning support is disabled and usage of {@code @Since} and {@code @Until}
* are ignored during serialization or deserialization. * has no effect.
*
* @param version the version number to use.
* @return a reference to this {@code GsonBuilder} object to fulfill the "Builder" pattern * @return a reference to this {@code GsonBuilder} object to fulfill the "Builder" pattern
* @throws IllegalArgumentException if the version number is NaN or negative
* @see Since
* @see Until
*/ */
public GsonBuilder setVersion(double ignoreVersionsAfter) { public GsonBuilder setVersion(double version) {
excluder = excluder.withVersion(ignoreVersionsAfter); if (Double.isNaN(version) || version < 0.0) {
throw new IllegalArgumentException("Invalid version: " + version);
}
excluder = excluder.withVersion(version);
return this; return this;
} }

View File

@ -16,6 +16,7 @@
package com.google.gson.annotations; package com.google.gson.annotations;
import com.google.gson.GsonBuilder;
import java.lang.annotation.Documented; import java.lang.annotation.Documented;
import java.lang.annotation.ElementType; import java.lang.annotation.ElementType;
import java.lang.annotation.Retention; import java.lang.annotation.Retention;
@ -24,12 +25,11 @@ import java.lang.annotation.Target;
/** /**
* An annotation that indicates the version number since a member or a type has been present. * An annotation that indicates the version number since a member or a type has been present.
* This annotation is useful to manage versioning of your Json classes for a web-service. * This annotation is useful to manage versioning of your JSON classes for a web-service.
* *
* <p> * <p>
* This annotation has no effect unless you build {@link com.google.gson.Gson} with a * This annotation has no effect unless you build {@link com.google.gson.Gson} with a
* {@link com.google.gson.GsonBuilder} and invoke * {@code GsonBuilder} and invoke the {@link GsonBuilder#setVersion(double)} method.
* {@link com.google.gson.GsonBuilder#setVersion(double)} method.
* *
* <p>Here is an example of how this annotation is meant to be used:</p> * <p>Here is an example of how this annotation is meant to be used:</p>
* <pre> * <pre>
@ -50,14 +50,16 @@ import java.lang.annotation.Target;
* *
* @author Inderjeet Singh * @author Inderjeet Singh
* @author Joel Leitch * @author Joel Leitch
* @see GsonBuilder#setVersion(double)
* @see Until
*/ */
@Documented @Documented
@Retention(RetentionPolicy.RUNTIME) @Retention(RetentionPolicy.RUNTIME)
@Target({ElementType.FIELD, ElementType.TYPE}) @Target({ElementType.FIELD, ElementType.TYPE})
public @interface Since { public @interface Since {
/** /**
* the value indicating a version number since this member * The value indicating a version number since this member or type has been present.
* or type has been present. * The number is inclusive; annotated elements will be included if {@code gsonVersion >= value}.
*/ */
double value(); double value();
} }

View File

@ -16,6 +16,7 @@
package com.google.gson.annotations; package com.google.gson.annotations;
import com.google.gson.GsonBuilder;
import java.lang.annotation.Documented; import java.lang.annotation.Documented;
import java.lang.annotation.ElementType; import java.lang.annotation.ElementType;
import java.lang.annotation.Retention; import java.lang.annotation.Retention;
@ -24,14 +25,13 @@ import java.lang.annotation.Target;
/** /**
* An annotation that indicates the version number until a member or a type should be present. * An annotation that indicates the version number until a member or a type should be present.
* Basically, if Gson is created with a version number that exceeds the value stored in the * Basically, if Gson is created with a version number that is equal to or exceeds the value
* {@code Until} annotation then the field will be ignored from the JSON output. This annotation * stored in the {@code Until} annotation then the field will be ignored from the JSON output.
* is useful to manage versioning of your JSON classes for a web-service. * This annotation is useful to manage versioning of your JSON classes for a web-service.
* *
* <p> * <p>
* This annotation has no effect unless you build {@link com.google.gson.Gson} with a * This annotation has no effect unless you build {@link com.google.gson.Gson} with a
* {@link com.google.gson.GsonBuilder} and invoke * {@code GsonBuilder} and invoke the {@link GsonBuilder#setVersion(double)} method.
* {@link com.google.gson.GsonBuilder#setVersion(double)} method.
* *
* <p>Here is an example of how this annotation is meant to be used:</p> * <p>Here is an example of how this annotation is meant to be used:</p>
* <pre> * <pre>
@ -53,6 +53,8 @@ import java.lang.annotation.Target;
* *
* @author Inderjeet Singh * @author Inderjeet Singh
* @author Joel Leitch * @author Joel Leitch
* @see GsonBuilder#setVersion(double)
* @see Since
* @since 1.3 * @since 1.3
*/ */
@Documented @Documented
@ -61,8 +63,8 @@ import java.lang.annotation.Target;
public @interface Until { public @interface Until {
/** /**
* the value indicating a version number until this member * The value indicating a version number until this member or type should be be included.
* or type should be ignored. * The number is exclusive; annotated elements will be included if {@code gsonVersion < value}.
*/ */
double value(); double value();
} }

View File

@ -240,9 +240,7 @@ public final class Excluder implements TypeAdapterFactory, Cloneable {
private boolean isValidSince(Since annotation) { private boolean isValidSince(Since annotation) {
if (annotation != null) { if (annotation != null) {
double annotationVersion = annotation.value(); double annotationVersion = annotation.value();
if (annotationVersion > version) { return version >= annotationVersion;
return false;
}
} }
return true; return true;
} }
@ -250,9 +248,7 @@ public final class Excluder implements TypeAdapterFactory, Cloneable {
private boolean isValidUntil(Until annotation) { private boolean isValidUntil(Until annotation) {
if (annotation != null) { if (annotation != null) {
double annotationVersion = annotation.value(); double annotationVersion = annotation.value();
if (annotationVersion <= version) { return version < annotationVersion;
return false;
}
} }
return true; return true;
} }

View File

@ -16,20 +16,25 @@
package com.google.gson; package com.google.gson;
import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertNotNull;
import static org.junit.Assert.assertNotSame;
import static org.junit.Assert.fail;
import com.google.gson.stream.JsonReader; import com.google.gson.stream.JsonReader;
import com.google.gson.stream.JsonWriter; import com.google.gson.stream.JsonWriter;
import java.io.IOException; import java.io.IOException;
import java.lang.reflect.Field; import java.lang.reflect.Field;
import java.lang.reflect.Modifier; import java.lang.reflect.Modifier;
import java.lang.reflect.Type; import java.lang.reflect.Type;
import junit.framework.TestCase; import org.junit.Test;
/** /**
* Unit tests for {@link GsonBuilder}. * Unit tests for {@link GsonBuilder}.
* *
* @author Inderjeet Singh * @author Inderjeet Singh
*/ */
public class GsonBuilderTest extends TestCase { public class GsonBuilderTest {
private static final TypeAdapter<Object> NULL_TYPE_ADAPTER = new TypeAdapter<Object>() { private static final TypeAdapter<Object> NULL_TYPE_ADAPTER = new TypeAdapter<Object>() {
@Override public void write(JsonWriter out, Object value) { @Override public void write(JsonWriter out, Object value) {
throw new AssertionError(); throw new AssertionError();
@ -39,6 +44,7 @@ public class GsonBuilderTest extends TestCase {
} }
}; };
@Test
public void testCreatingMoreThanOnce() { public void testCreatingMoreThanOnce() {
GsonBuilder builder = new GsonBuilder(); GsonBuilder builder = new GsonBuilder();
Gson gson = builder.create(); Gson gson = builder.create();
@ -61,6 +67,7 @@ public class GsonBuilderTest extends TestCase {
* Gson instances should not be affected by subsequent modification of GsonBuilder * Gson instances should not be affected by subsequent modification of GsonBuilder
* which created them. * which created them.
*/ */
@Test
public void testModificationAfterCreate() { public void testModificationAfterCreate() {
GsonBuilder gsonBuilder = new GsonBuilder(); GsonBuilder gsonBuilder = new GsonBuilder();
Gson gson = gsonBuilder.create(); Gson gson = gsonBuilder.create();
@ -136,6 +143,7 @@ public class GsonBuilderTest extends TestCase {
} }
} }
@Test
public void testExcludeFieldsWithModifiers() { public void testExcludeFieldsWithModifiers() {
Gson gson = new GsonBuilder() Gson gson = new GsonBuilder()
.excludeFieldsWithModifiers(Modifier.VOLATILE, Modifier.PRIVATE) .excludeFieldsWithModifiers(Modifier.VOLATILE, Modifier.PRIVATE)
@ -151,6 +159,7 @@ public class GsonBuilderTest extends TestCase {
String d = "d"; String d = "d";
} }
@Test
public void testTransientFieldExclusion() { public void testTransientFieldExclusion() {
Gson gson = new GsonBuilder() Gson gson = new GsonBuilder()
.excludeFieldsWithModifiers() .excludeFieldsWithModifiers()
@ -162,6 +171,7 @@ public class GsonBuilderTest extends TestCase {
transient String a = "a"; transient String a = "a";
} }
@Test
public void testRegisterTypeAdapterForCoreType() { public void testRegisterTypeAdapterForCoreType() {
Type[] types = { Type[] types = {
byte.class, byte.class,
@ -176,6 +186,7 @@ public class GsonBuilderTest extends TestCase {
} }
} }
@Test
public void testDisableJdkUnsafe() { public void testDisableJdkUnsafe() {
Gson gson = new GsonBuilder() Gson gson = new GsonBuilder()
.disableJdkUnsafe() .disableJdkUnsafe()
@ -198,4 +209,22 @@ public class GsonBuilderTest extends TestCase {
public ClassWithoutNoArgsConstructor(String s) { public ClassWithoutNoArgsConstructor(String s) {
} }
} }
@Test
public void testSetVersionInvalid() {
GsonBuilder builder = new GsonBuilder();
try {
builder.setVersion(Double.NaN);
fail();
} catch (IllegalArgumentException e) {
assertEquals("Invalid version: NaN", e.getMessage());
}
try {
builder.setVersion(-0.1);
fail();
} catch (IllegalArgumentException e) {
assertEquals("Invalid version: -0.1", e.getMessage());
}
}
} }

View File

@ -16,40 +16,82 @@
package com.google.gson; package com.google.gson;
import static org.junit.Assert.assertFalse;
import static org.junit.Assert.assertTrue;
import com.google.gson.annotations.Since; import com.google.gson.annotations.Since;
import com.google.gson.annotations.Until;
import com.google.gson.internal.Excluder; import com.google.gson.internal.Excluder;
import junit.framework.TestCase; import org.junit.Test;
/** /**
* Unit tests for the {@link Excluder} class. * Unit tests for the {@link Excluder} class.
* *
* @author Joel Leitch * @author Joel Leitch
*/ */
public class VersionExclusionStrategyTest extends TestCase { public class VersionExclusionStrategyTest {
private static final double VERSION = 5.0D; private static final double VERSION = 5.0D;
public void testClassAndFieldAreAtSameVersion() throws Exception { @Test
public void testSameVersion() throws Exception {
Excluder excluder = Excluder.DEFAULT.withVersion(VERSION); Excluder excluder = Excluder.DEFAULT.withVersion(VERSION);
assertFalse(excluder.excludeClass(MockObject.class, true)); assertFalse(excluder.excludeClass(MockClassSince.class, true));
assertFalse(excluder.excludeField(MockObject.class.getField("someField"), true)); assertFalse(excluder.excludeField(MockClassSince.class.getField("someField"), true));
// Until version is exclusive
assertTrue(excluder.excludeClass(MockClassUntil.class, true));
assertTrue(excluder.excludeField(MockClassUntil.class.getField("someField"), true));
assertFalse(excluder.excludeClass(MockClassBoth.class, true));
assertFalse(excluder.excludeField(MockClassBoth.class.getField("someField"), true));
} }
public void testClassAndFieldAreBehindInVersion() throws Exception { @Test
Excluder excluder = Excluder.DEFAULT.withVersion(VERSION + 1); public void testNewerVersion() throws Exception {
assertFalse(excluder.excludeClass(MockObject.class, true)); Excluder excluder = Excluder.DEFAULT.withVersion(VERSION + 5);
assertFalse(excluder.excludeField(MockObject.class.getField("someField"), true)); assertFalse(excluder.excludeClass(MockClassSince.class, true));
assertFalse(excluder.excludeField(MockClassSince.class.getField("someField"), true));
assertTrue(excluder.excludeClass(MockClassUntil.class, true));
assertTrue(excluder.excludeField(MockClassUntil.class.getField("someField"), true));
assertTrue(excluder.excludeClass(MockClassBoth.class, true));
assertTrue(excluder.excludeField(MockClassBoth.class.getField("someField"), true));
} }
public void testClassAndFieldAreAheadInVersion() throws Exception { @Test
Excluder excluder = Excluder.DEFAULT.withVersion(VERSION - 1); public void testOlderVersion() throws Exception {
assertTrue(excluder.excludeClass(MockObject.class, true)); Excluder excluder = Excluder.DEFAULT.withVersion(VERSION - 5);
assertTrue(excluder.excludeField(MockObject.class.getField("someField"), true)); assertTrue(excluder.excludeClass(MockClassSince.class, true));
assertTrue(excluder.excludeField(MockClassSince.class.getField("someField"), true));
assertFalse(excluder.excludeClass(MockClassUntil.class, true));
assertFalse(excluder.excludeField(MockClassUntil.class.getField("someField"), true));
assertTrue(excluder.excludeClass(MockClassBoth.class, true));
assertTrue(excluder.excludeField(MockClassBoth.class.getField("someField"), true));
} }
@Since(VERSION) @Since(VERSION)
private static class MockObject { private static class MockClassSince {
@Since(VERSION) @Since(VERSION)
public final int someField = 0; public final int someField = 0;
} }
@Until(VERSION)
private static class MockClassUntil {
@Until(VERSION)
public final int someField = 0;
}
@Since(VERSION)
@Until(VERSION + 2)
private static class MockClassBoth {
@Since(VERSION)
@Until(VERSION + 2)
public final int someField = 0;
}
} }

View File

@ -15,13 +15,17 @@
*/ */
package com.google.gson.functional; package com.google.gson.functional;
import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertFalse;
import static org.junit.Assert.assertNull;
import static org.junit.Assert.assertTrue;
import com.google.gson.Gson; import com.google.gson.Gson;
import com.google.gson.GsonBuilder; import com.google.gson.GsonBuilder;
import com.google.gson.annotations.Since; import com.google.gson.annotations.Since;
import com.google.gson.annotations.Until; import com.google.gson.annotations.Until;
import com.google.gson.common.TestTypes.BagOfPrimitives; import com.google.gson.common.TestTypes.BagOfPrimitives;
import org.junit.Test;
import junit.framework.TestCase;
/** /**
* Functional tests for versioning support in Gson. * Functional tests for versioning support in Gson.
@ -29,47 +33,60 @@ import junit.framework.TestCase;
* @author Inderjeet Singh * @author Inderjeet Singh
* @author Joel Leitch * @author Joel Leitch
*/ */
public class VersioningTest extends TestCase { public class VersioningTest {
private static final int A = 0; private static final int A = 0;
private static final int B = 1; private static final int B = 1;
private static final int C = 2; private static final int C = 2;
private static final int D = 3; private static final int D = 3;
private GsonBuilder builder; private static Gson gsonWithVersion(double version) {
return new GsonBuilder().setVersion(version).create();
@Override
protected void setUp() throws Exception {
super.setUp();
builder = new GsonBuilder();
} }
@Test
public void testVersionedUntilSerialization() { public void testVersionedUntilSerialization() {
Version1 target = new Version1(); Version1 target = new Version1();
Gson gson = builder.setVersion(1.29).create(); Gson gson = gsonWithVersion(1.29);
String json = gson.toJson(target); String json = gson.toJson(target);
assertTrue(json.contains("\"a\":" + A)); assertTrue(json.contains("\"a\":" + A));
gson = builder.setVersion(1.3).create(); gson = gsonWithVersion(1.3);
json = gson.toJson(target);
assertFalse(json.contains("\"a\":" + A));
gson = gsonWithVersion(1.31);
json = gson.toJson(target); json = gson.toJson(target);
assertFalse(json.contains("\"a\":" + A)); assertFalse(json.contains("\"a\":" + A));
} }
@Test
public void testVersionedUntilDeserialization() { public void testVersionedUntilDeserialization() {
Gson gson = builder.setVersion(1.3).create();
String json = "{\"a\":3,\"b\":4,\"c\":5}"; String json = "{\"a\":3,\"b\":4,\"c\":5}";
Gson gson = gsonWithVersion(1.29);
Version1 version1 = gson.fromJson(json, Version1.class); Version1 version1 = gson.fromJson(json, Version1.class);
assertEquals(3, version1.a);
gson = gsonWithVersion(1.3);
version1 = gson.fromJson(json, Version1.class);
assertEquals(A, version1.a);
gson = gsonWithVersion(1.31);
version1 = gson.fromJson(json, Version1.class);
assertEquals(A, version1.a); assertEquals(A, version1.a);
} }
@Test
public void testVersionedClassesSerialization() { public void testVersionedClassesSerialization() {
Gson gson = builder.setVersion(1.0).create(); Gson gson = gsonWithVersion(1.0);
String json1 = gson.toJson(new Version1()); String json1 = gson.toJson(new Version1());
String json2 = gson.toJson(new Version1_1()); String json2 = gson.toJson(new Version1_1());
assertEquals(json1, json2); assertEquals(json1, json2);
} }
@Test
public void testVersionedClassesDeserialization() { public void testVersionedClassesDeserialization() {
Gson gson = builder.setVersion(1.0).create(); Gson gson = gsonWithVersion(1.0);
String json = "{\"a\":3,\"b\":4,\"c\":5}"; String json = "{\"a\":3,\"b\":4,\"c\":5}";
Version1 version1 = gson.fromJson(json, Version1.class); Version1 version1 = gson.fromJson(json, Version1.class);
assertEquals(3, version1.a); assertEquals(3, version1.a);
@ -80,13 +97,15 @@ public class VersioningTest extends TestCase {
assertEquals(C, version1_1.c); assertEquals(C, version1_1.c);
} }
@Test
public void testIgnoreLaterVersionClassSerialization() { public void testIgnoreLaterVersionClassSerialization() {
Gson gson = builder.setVersion(1.0).create(); Gson gson = gsonWithVersion(1.0);
assertEquals("null", gson.toJson(new Version1_2())); assertEquals("null", gson.toJson(new Version1_2()));
} }
@Test
public void testIgnoreLaterVersionClassDeserialization() { public void testIgnoreLaterVersionClassDeserialization() {
Gson gson = builder.setVersion(1.0).create(); Gson gson = gsonWithVersion(1.0);
String json = "{\"a\":3,\"b\":4,\"c\":5,\"d\":6}"; String json = "{\"a\":3,\"b\":4,\"c\":5,\"d\":6}";
Version1_2 version1_2 = gson.fromJson(json, Version1_2.class); Version1_2 version1_2 = gson.fromJson(json, Version1_2.class);
// Since the class is versioned to be after 1.0, we expect null // Since the class is versioned to be after 1.0, we expect null
@ -94,14 +113,16 @@ public class VersioningTest extends TestCase {
assertNull(version1_2); assertNull(version1_2);
} }
@Test
public void testVersionedGsonWithUnversionedClassesSerialization() { public void testVersionedGsonWithUnversionedClassesSerialization() {
Gson gson = builder.setVersion(1.0).create(); Gson gson = gsonWithVersion(1.0);
BagOfPrimitives target = new BagOfPrimitives(10, 20, false, "stringValue"); BagOfPrimitives target = new BagOfPrimitives(10, 20, false, "stringValue");
assertEquals(target.getExpectedJson(), gson.toJson(target)); assertEquals(target.getExpectedJson(), gson.toJson(target));
} }
@Test
public void testVersionedGsonWithUnversionedClassesDeserialization() { public void testVersionedGsonWithUnversionedClassesDeserialization() {
Gson gson = builder.setVersion(1.0).create(); Gson gson = gsonWithVersion(1.0);
String json = "{\"longValue\":10,\"intValue\":20,\"booleanValue\":false}"; String json = "{\"longValue\":10,\"intValue\":20,\"booleanValue\":false}";
BagOfPrimitives expected = new BagOfPrimitives(); BagOfPrimitives expected = new BagOfPrimitives();
@ -112,34 +133,45 @@ public class VersioningTest extends TestCase {
assertEquals(expected, actual); assertEquals(expected, actual);
} }
@Test
public void testVersionedGsonMixingSinceAndUntilSerialization() { public void testVersionedGsonMixingSinceAndUntilSerialization() {
Gson gson = builder.setVersion(1.0).create(); Gson gson = gsonWithVersion(1.0);
SinceUntilMixing target = new SinceUntilMixing(); SinceUntilMixing target = new SinceUntilMixing();
String json = gson.toJson(target); String json = gson.toJson(target);
assertFalse(json.contains("\"b\":" + B)); assertFalse(json.contains("\"b\":" + B));
gson = builder.setVersion(1.2).create(); gson = gsonWithVersion(1.2);
json = gson.toJson(target); json = gson.toJson(target);
assertTrue(json.contains("\"b\":" + B)); assertTrue(json.contains("\"b\":" + B));
gson = builder.setVersion(1.3).create(); gson = gsonWithVersion(1.3);
json = gson.toJson(target);
assertFalse(json.contains("\"b\":" + B));
gson = gsonWithVersion(1.4);
json = gson.toJson(target); json = gson.toJson(target);
assertFalse(json.contains("\"b\":" + B)); assertFalse(json.contains("\"b\":" + B));
} }
@Test
public void testVersionedGsonMixingSinceAndUntilDeserialization() { public void testVersionedGsonMixingSinceAndUntilDeserialization() {
String json = "{\"a\":5,\"b\":6}"; String json = "{\"a\":5,\"b\":6}";
Gson gson = builder.setVersion(1.0).create(); Gson gson = gsonWithVersion(1.0);
SinceUntilMixing result = gson.fromJson(json, SinceUntilMixing.class); SinceUntilMixing result = gson.fromJson(json, SinceUntilMixing.class);
assertEquals(5, result.a); assertEquals(5, result.a);
assertEquals(B, result.b); assertEquals(B, result.b);
gson = builder.setVersion(1.2).create(); gson = gsonWithVersion(1.2);
result = gson.fromJson(json, SinceUntilMixing.class); result = gson.fromJson(json, SinceUntilMixing.class);
assertEquals(5, result.a); assertEquals(5, result.a);
assertEquals(6, result.b); assertEquals(6, result.b);
gson = builder.setVersion(1.3).create(); gson = gsonWithVersion(1.3);
result = gson.fromJson(json, SinceUntilMixing.class);
assertEquals(5, result.a);
assertEquals(B, result.b);
gson = gsonWithVersion(1.4);
result = gson.fromJson(json, SinceUntilMixing.class); result = gson.fromJson(json, SinceUntilMixing.class);
assertEquals(5, result.a); assertEquals(5, result.a);
assertEquals(B, result.b); assertEquals(B, result.b);