Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Improve versioning support documentation and validate version #2214

Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
23 changes: 18 additions & 5 deletions gson/src/main/java/com/google/gson/GsonBuilder.java
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,8 @@
import static com.google.gson.Gson.DEFAULT_SPECIALIZE_FLOAT_VALUES;
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.Excluder;
import com.google.gson.internal.bind.DefaultDateTypeAdapter;
Expand Down Expand Up @@ -143,14 +145,25 @@ public 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
* are ignored during serialization or deserialization.
* <p>By default versioning support is disabled and usage of {@code @Since} and {@code @Until}
* has no effect.
*
* @param version the version number to use.
* @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) {
excluder = excluder.withVersion(ignoreVersionsAfter);
public GsonBuilder setVersion(double version) {
if (Double.isNaN(version) || version < 0.0) {
Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Have added this < 0.0 check because negative version numbers are probably rather uncommon and this could cause collisions with the undocumented value -1.0 representing no version:

private static final double IGNORE_VERSIONS = -1.0d;

throw new IllegalArgumentException("Invalid version: " + version);
}
excluder = excluder.withVersion(version);
return this;
}

Expand Down
12 changes: 7 additions & 5 deletions gson/src/main/java/com/google/gson/annotations/Since.java
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@

package com.google.gson.annotations;

import com.google.gson.GsonBuilder;
import java.lang.annotation.Documented;
import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
Expand All @@ -24,12 +25,11 @@

/**
* 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>
* This annotation has no effect unless you build {@link com.google.gson.Gson} with a
* {@link com.google.gson.GsonBuilder} and invoke
* {@link com.google.gson.GsonBuilder#setVersion(double)} method.
* {@code GsonBuilder} and invoke the {@link GsonBuilder#setVersion(double)} method.
*
* <p>Here is an example of how this annotation is meant to be used:</p>
* <pre>
Expand All @@ -50,14 +50,16 @@
*
* @author Inderjeet Singh
* @author Joel Leitch
* @see GsonBuilder#setVersion(double)
* @see Until
*/
@Documented
@Retention(RetentionPolicy.RUNTIME)
@Target({ElementType.FIELD, ElementType.TYPE})
public @interface Since {
/**
* the value indicating a version number since this member
* or type has been present.
* The value indicating a version number since this member or type has been present.
* The number is inclusive; annotated elements will be included if {@code gsonVersion >= value}.
*/
double value();
}
18 changes: 10 additions & 8 deletions gson/src/main/java/com/google/gson/annotations/Until.java
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@

package com.google.gson.annotations;

import com.google.gson.GsonBuilder;
import java.lang.annotation.Documented;
import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
Expand All @@ -24,14 +25,13 @@

/**
* 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
* {@code Until} annotation then the field will be ignored from the JSON output. This annotation
* is useful to manage versioning of your JSON classes for a web-service.
* Basically, if Gson is created with a version number that is equal to or exceeds the value
* stored in the {@code Until} annotation then the field will be ignored from the JSON output.
* This annotation is useful to manage versioning of your JSON classes for a web-service.
*
* <p>
* This annotation has no effect unless you build {@link com.google.gson.Gson} with a
* {@link com.google.gson.GsonBuilder} and invoke
* {@link com.google.gson.GsonBuilder#setVersion(double)} method.
* {@code GsonBuilder} and invoke the {@link GsonBuilder#setVersion(double)} method.
*
* <p>Here is an example of how this annotation is meant to be used:</p>
* <pre>
Expand All @@ -47,12 +47,14 @@
* methods will use all the fields for serialization and deserialization. However, if you created
* Gson with {@code Gson gson = new GsonBuilder().setVersion(1.2).create()} then the
* {@code toJson()} and {@code fromJson()} methods of Gson will exclude the {@code emailAddress}
* and {@code password} fields from the example above, because the version number passed to the
* and {@code password} fields from the example above, because the version number passed to the
* GsonBuilder, {@code 1.2}, exceeds the version number set on the {@code Until} annotation,
* {@code 1.1}, for those fields.
*
* @author Inderjeet Singh
* @author Joel Leitch
* @see GsonBuilder#setVersion(double)
* @see Since
* @since 1.3
*/
@Documented
Expand All @@ -61,8 +63,8 @@
public @interface Until {

/**
* the value indicating a version number until this member
* or type should be ignored.
* The value indicating a version number until this member or type should be be included.
* The number is exclusive; annotated elements will be included if {@code gsonVersion < value}.
*/
double value();
}
8 changes: 2 additions & 6 deletions gson/src/main/java/com/google/gson/internal/Excluder.java
Original file line number Diff line number Diff line change
Expand Up @@ -240,19 +240,15 @@ private boolean isValidVersion(Since since, Until until) {
private boolean isValidSince(Since annotation) {
if (annotation != null) {
double annotationVersion = annotation.value();
if (annotationVersion > version) {
return false;
}
return version >= annotationVersion;
Copy link
Collaborator Author

@Marcono1234 Marcono1234 Oct 2, 2022

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This should behave equivalently (unless someone uses NaN as version ...), but is hopefully a bit easier to understand.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Indeed, the old code was surprising.

}
return true;
}

private boolean isValidUntil(Until annotation) {
if (annotation != null) {
double annotationVersion = annotation.value();
if (annotationVersion <= version) {
return false;
}
return version < annotationVersion;
Copy link
Collaborator Author

@Marcono1234 Marcono1234 Oct 2, 2022

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This should behave equivalently (unless someone uses NaN as version ...), but is hopefully a bit easier to understand.

}
return true;
}
Expand Down
33 changes: 31 additions & 2 deletions gson/src/test/java/com/google/gson/GsonBuilderTest.java
Original file line number Diff line number Diff line change
Expand Up @@ -16,20 +16,25 @@

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.JsonWriter;
import java.io.IOException;
import java.lang.reflect.Field;
import java.lang.reflect.Modifier;
import java.lang.reflect.Type;
import junit.framework.TestCase;
import org.junit.Test;

/**
* Unit tests for {@link GsonBuilder}.
*
* @author Inderjeet Singh
*/
public class GsonBuilderTest extends TestCase {
public class GsonBuilderTest {
private static final TypeAdapter<Object> NULL_TYPE_ADAPTER = new TypeAdapter<Object>() {
@Override public void write(JsonWriter out, Object value) {
throw new AssertionError();
Expand All @@ -39,6 +44,7 @@ public class GsonBuilderTest extends TestCase {
}
};

@Test
public void testCreatingMoreThanOnce() {
GsonBuilder builder = new GsonBuilder();
Gson gson = builder.create();
Expand All @@ -61,6 +67,7 @@ public void testCreatingMoreThanOnce() {
* Gson instances should not be affected by subsequent modification of GsonBuilder
* which created them.
*/
@Test
public void testModificationAfterCreate() {
GsonBuilder gsonBuilder = new GsonBuilder();
Gson gson = gsonBuilder.create();
Expand Down Expand Up @@ -136,6 +143,7 @@ public CustomClass3() {
}
}

@Test
public void testExcludeFieldsWithModifiers() {
Gson gson = new GsonBuilder()
.excludeFieldsWithModifiers(Modifier.VOLATILE, Modifier.PRIVATE)
Expand All @@ -151,6 +159,7 @@ static class HasModifiers {
String d = "d";
}

@Test
public void testTransientFieldExclusion() {
Gson gson = new GsonBuilder()
.excludeFieldsWithModifiers()
Expand All @@ -162,6 +171,7 @@ static class HasTransients {
transient String a = "a";
}

@Test
public void testRegisterTypeAdapterForCoreType() {
Type[] types = {
byte.class,
Expand All @@ -176,6 +186,7 @@ public void testRegisterTypeAdapterForCoreType() {
}
}

@Test
public void testDisableJdkUnsafe() {
Gson gson = new GsonBuilder()
.disableJdkUnsafe()
Expand All @@ -198,4 +209,22 @@ private static class ClassWithoutNoArgsConstructor {
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());
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -16,40 +16,82 @@

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.Until;
import com.google.gson.internal.Excluder;
import junit.framework.TestCase;
import org.junit.Test;

/**
* Unit tests for the {@link Excluder} class.
*
* @author Joel Leitch
*/
public class VersionExclusionStrategyTest extends TestCase {
public class VersionExclusionStrategyTest {
private static final double VERSION = 5.0D;

public void testClassAndFieldAreAtSameVersion() throws Exception {
@Test
public void testSameVersion() throws Exception {
Excluder excluder = Excluder.DEFAULT.withVersion(VERSION);
assertFalse(excluder.excludeClass(MockObject.class, true));
assertFalse(excluder.excludeField(MockObject.class.getField("someField"), true));
assertFalse(excluder.excludeClass(MockClassSince.class, 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 {
Excluder excluder = Excluder.DEFAULT.withVersion(VERSION + 1);
assertFalse(excluder.excludeClass(MockObject.class, true));
assertFalse(excluder.excludeField(MockObject.class.getField("someField"), true));
@Test
public void testNewerVersion() throws Exception {
Excluder excluder = Excluder.DEFAULT.withVersion(VERSION + 5);
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));
}

@Test
public void testOlderVersion() throws Exception {
Excluder excluder = Excluder.DEFAULT.withVersion(VERSION - 5);
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));
}

public void testClassAndFieldAreAheadInVersion() throws Exception {
Excluder excluder = Excluder.DEFAULT.withVersion(VERSION - 1);
assertTrue(excluder.excludeClass(MockObject.class, true));
assertTrue(excluder.excludeField(MockObject.class.getField("someField"), true));
@Since(VERSION)
private static class MockClassSince {

@Since(VERSION)
public final int someField = 0;
}

@Until(VERSION)
private static class MockClassUntil {

@Until(VERSION)
public final int someField = 0;
}

@Since(VERSION)
private static class MockObject {
@Until(VERSION + 2)
private static class MockClassBoth {

@Since(VERSION)
@Until(VERSION + 2)
public final int someField = 0;
}
}
Loading