From bb7f0b6bb01b0e98e32229cb528772a66a9d6075 Mon Sep 17 00:00:00 2001 From: Jesse Wilson Date: Fri, 30 Sep 2011 07:08:44 +0000 Subject: [PATCH] Adopt JsonElementWriter in GSON. Add setSerializeNulls() to JsonWriter, so nulls can be skipped from serialization. This does not yet impact JsonElementWriter. One change in behavior: if the only value is skipped, we now emit "null" rather than "". --- gson/GSON 2.0 NOTES.txt | 14 +++-- gson/src/main/java/com/google/gson/Gson.java | 54 +++++++++++++++---- .../com/google/gson/stream/JsonWriter.java | 52 +++++++++++++++++- .../gson/functional/FieldExclusionTest.java | 22 ++++---- .../google/gson/functional/ObjectTest.java | 30 +++++------ .../gson/functional/VersioningTest.java | 18 +++---- .../internal/bind/JsonElementWriterTest.java | 1 + 7 files changed, 140 insertions(+), 51 deletions(-) diff --git a/gson/GSON 2.0 NOTES.txt b/gson/GSON 2.0 NOTES.txt index 3e53fc40..1896c8ec 100644 --- a/gson/GSON 2.0 NOTES.txt +++ b/gson/GSON 2.0 NOTES.txt @@ -54,8 +54,16 @@ GSON 1.x sometimes sets subclass fields when an InstanceCreator returns a subcla GSON 2.x sets fields of the requested type only com.google.gson.functional.InstanceCreatorTest.testInstanceCreatorReturnsSubTypeForField + GSON 1.x applies different rules for versioning for classes vs fields. So, if you deserialize a - JSON into a field that is supposed to be skipped, the field is set to null (or default value). + JSON into a field that is supposed to be skipped, the field is set to null (or default value). However, if you deserialize it to a top-level class, a default instance is returned. -GSON 2.x returns null for the top-level class. -com.google.gson.functional.VersioningTest.testIgnoreLaterVersionClassDeserialization \ No newline at end of file +GSON 2.x returns null for the top-level class. +com.google.gson.functional.VersioningTest.testIgnoreLaterVersionClassDeserialization + + +GSON 1.x creates the empty string "" if the only element is skipped +GSON 2.x writes "null" if the only element is skipped +com.google.gson.functional.ObjectTest.testAnonymousLocalClassesSerialization +com.google.gson.functional.FieldExclusionTest.testInnerClassExclusion +com.google.gson.functional.VersioningTest.testIgnoreLaterVersionClassSerialization \ No newline at end of file diff --git a/gson/src/main/java/com/google/gson/Gson.java b/gson/src/main/java/com/google/gson/Gson.java index 82ac2338..fe5130b8 100644 --- a/gson/src/main/java/com/google/gson/Gson.java +++ b/gson/src/main/java/com/google/gson/Gson.java @@ -443,7 +443,7 @@ public final class Gson { */ public String toJson(Object src, Type typeOfSrc) { StringWriter writer = new StringWriter(); - toJson(toJsonTree(src, typeOfSrc), writer); + toJson(src, typeOfSrc, writer); return writer.toString(); } @@ -486,8 +486,12 @@ public final class Gson { * @since 1.2 */ public void toJson(Object src, Type typeOfSrc, Appendable writer) throws JsonIOException { - JsonElement jsonElement = toJsonTree(src, typeOfSrc); - toJson(jsonElement, writer); + try { + JsonWriter jsonWriter = newJsonWriter(Streams.writerForAppendable(writer)); + toJson(src, typeOfSrc, jsonWriter); + } catch (IOException e) { + throw new JsonIOException(e); + } } /** @@ -496,7 +500,22 @@ public final class Gson { * @throws JsonIOException if there was a problem writing to the writer */ public void toJson(Object src, Type typeOfSrc, JsonWriter writer) throws JsonIOException { - toJson(toJsonTree(src, typeOfSrc), writer); + TypeAdapter adapter = miniGson.getAdapter(TypeToken.get(typeOfSrc)); + boolean oldLenient = writer.isLenient(); + writer.setLenient(true); + boolean oldHtmlSafe = writer.isHtmlSafe(); + writer.setHtmlSafe(htmlSafe); + boolean oldSerializeNulls = writer.getSerializeNulls(); + writer.setSerializeNulls(serializeNulls); + try { + ((TypeAdapter) adapter).write(writer, src); + } catch (IOException e) { + throw new JsonIOException(e); + } finally { + writer.setLenient(oldLenient); + writer.setHtmlSafe(oldHtmlSafe); + writer.setSerializeNulls(oldSerializeNulls); + } } /** @@ -522,19 +541,29 @@ public final class Gson { */ public void toJson(JsonElement jsonElement, Appendable writer) throws JsonIOException { try { - if (generateNonExecutableJson) { - writer.append(JSON_NON_EXECUTABLE_PREFIX); - } - JsonWriter jsonWriter = new JsonWriter(Streams.writerForAppendable(writer)); - if (prettyPrinting) { - jsonWriter.setIndent(" "); - } + JsonWriter jsonWriter = newJsonWriter(Streams.writerForAppendable(writer)); toJson(jsonElement, jsonWriter); } catch (IOException e) { throw new RuntimeException(e); } } + /** + * Returns a new JSON writer configured for this GSON and with the non-execute + * prefix if that is configured. + */ + private JsonWriter newJsonWriter(Writer writer) throws IOException { + if (generateNonExecutableJson) { + writer.write(JSON_NON_EXECUTABLE_PREFIX); + } + JsonWriter jsonWriter = new JsonWriter(writer); + if (prettyPrinting) { + jsonWriter.setIndent(" "); + } + jsonWriter.setSerializeNulls(serializeNulls); + return jsonWriter; + } + /** * Writes the JSON for {@code jsonElement} to {@code writer}. * @throws JsonIOException if there was a problem writing to the writer @@ -544,6 +573,8 @@ public final class Gson { writer.setLenient(true); boolean oldHtmlSafe = writer.isHtmlSafe(); writer.setHtmlSafe(htmlSafe); + boolean oldSerializeNulls = writer.getSerializeNulls(); + writer.setSerializeNulls(serializeNulls); try { Streams.write(jsonElement, serializeNulls, writer); } catch (IOException e) { @@ -551,6 +582,7 @@ public final class Gson { } finally { writer.setLenient(oldLenient); writer.setHtmlSafe(oldHtmlSafe); + writer.setSerializeNulls(oldSerializeNulls); } } diff --git a/gson/src/main/java/com/google/gson/stream/JsonWriter.java b/gson/src/main/java/com/google/gson/stream/JsonWriter.java index 49d16d11..8bd8ee8e 100644 --- a/gson/src/main/java/com/google/gson/stream/JsonWriter.java +++ b/gson/src/main/java/com/google/gson/stream/JsonWriter.java @@ -145,6 +145,10 @@ public class JsonWriter implements Closeable { private boolean htmlSafe; + private String deferredName; + + private boolean serializeNulls = true; + /** * Creates a new instance that writes a JSON-encoded stream to {@code out}. * For best performance, ensure {@link Writer} is buffered; wrapping in @@ -217,6 +221,22 @@ public class JsonWriter implements Closeable { return htmlSafe; } + /** + * Sets whether object members are serialized when their value is null. + * This has no impact on array elements. The default is true. + */ + public final void setSerializeNulls(boolean serializeNulls) { + this.serializeNulls = serializeNulls; + } + + /** + * Returns true if object members are serialized when their value is null. + * This has no impact on array elements. The default is true. + */ + public final boolean getSerializeNulls() { + return serializeNulls; + } + /** * Begins encoding a new array. Each call to this method must be paired with * a call to {@link #endArray}. @@ -224,6 +244,7 @@ public class JsonWriter implements Closeable { * @return this writer. */ public JsonWriter beginArray() throws IOException { + writeDeferredName(); return open(JsonScope.EMPTY_ARRAY, "["); } @@ -243,6 +264,7 @@ public class JsonWriter implements Closeable { * @return this writer. */ public JsonWriter beginObject() throws IOException { + writeDeferredName(); return open(JsonScope.EMPTY_OBJECT, "{"); } @@ -276,6 +298,9 @@ public class JsonWriter implements Closeable { if (context != nonempty && context != empty) { throw new IllegalStateException("Nesting problem: " + stack); } + if (deferredName != null) { + throw new IllegalStateException("Dangling name: " + deferredName); + } stack.remove(stack.size() - 1); if (context == nonempty) { @@ -309,11 +334,21 @@ public class JsonWriter implements Closeable { if (name == null) { throw new NullPointerException("name == null"); } - beforeName(); - string(name); + if (deferredName != null) { + throw new IllegalStateException(); + } + deferredName = name; return this; } + private void writeDeferredName() throws IOException { + if (deferredName != null) { + beforeName(); + string(deferredName); + deferredName = null; + } + } + /** * Encodes {@code value}. * @@ -324,6 +359,7 @@ public class JsonWriter implements Closeable { if (value == null) { return nullValue(); } + writeDeferredName(); beforeValue(false); string(value); return this; @@ -335,6 +371,14 @@ public class JsonWriter implements Closeable { * @return this writer. */ public JsonWriter nullValue() throws IOException { + if (deferredName != null) { + if (serializeNulls) { + writeDeferredName(); + } else { + deferredName = null; + return this; // skip the name and the value + } + } beforeValue(false); out.write("null"); return this; @@ -346,6 +390,7 @@ public class JsonWriter implements Closeable { * @return this writer. */ public JsonWriter value(boolean value) throws IOException { + writeDeferredName(); beforeValue(false); out.write(value ? "true" : "false"); return this; @@ -362,6 +407,7 @@ public class JsonWriter implements Closeable { if (Double.isNaN(value) || Double.isInfinite(value)) { throw new IllegalArgumentException("Numeric values must be finite, but was " + value); } + writeDeferredName(); beforeValue(false); out.append(Double.toString(value)); return this; @@ -373,6 +419,7 @@ public class JsonWriter implements Closeable { * @return this writer. */ public JsonWriter value(long value) throws IOException { + writeDeferredName(); beforeValue(false); out.write(Long.toString(value)); return this; @@ -390,6 +437,7 @@ public class JsonWriter implements Closeable { return nullValue(); } + writeDeferredName(); String string = value.toString(); if (!lenient && (string.equals("-Infinity") || string.equals("Infinity") || string.equals("NaN"))) { diff --git a/gson/src/test/java/com/google/gson/functional/FieldExclusionTest.java b/gson/src/test/java/com/google/gson/functional/FieldExclusionTest.java index 31f67cee..080a8234 100644 --- a/gson/src/test/java/com/google/gson/functional/FieldExclusionTest.java +++ b/gson/src/test/java/com/google/gson/functional/FieldExclusionTest.java @@ -32,59 +32,59 @@ public class FieldExclusionTest extends TestCase { private static final String VALUE = "blah_1234"; private Outer outer; - + @Override protected void setUp() throws Exception { super.setUp(); outer = new Outer(); } - + public void testDefaultInnerClassExclusion() throws Exception { Gson gson = new Gson(); Outer.Inner target = outer.new Inner(VALUE); String result = gson.toJson(target); assertEquals(target.toJson(), result); - + gson = new GsonBuilder().create(); target = outer.new Inner(VALUE); result = gson.toJson(target); assertEquals(target.toJson(), result); } - + public void testInnerClassExclusion() throws Exception { Gson gson = new GsonBuilder().disableInnerClassSerialization().create(); Outer.Inner target = outer.new Inner(VALUE); String result = gson.toJson(target); - assertEquals("", result); + assertEquals("null", result); } - + public void testDefaultNestedStaticClassIncluded() throws Exception { Gson gson = new Gson(); Outer.Inner target = outer.new Inner(VALUE); String result = gson.toJson(target); assertEquals(target.toJson(), result); - + gson = new GsonBuilder().create(); target = outer.new Inner(VALUE); result = gson.toJson(target); assertEquals(target.toJson(), result); } - + private static class Outer { private class Inner extends NestedClass { public Inner(String value) { super(value); } } - + } - + private static class NestedClass { private final String value; public NestedClass(String value) { this.value = value; } - + public String toJson() { return "{\"value\":\"" + value + "\"}"; } diff --git a/gson/src/test/java/com/google/gson/functional/ObjectTest.java b/gson/src/test/java/com/google/gson/functional/ObjectTest.java index 0bb72802..327ae0b9 100644 --- a/gson/src/test/java/com/google/gson/functional/ObjectTest.java +++ b/gson/src/test/java/com/google/gson/functional/ObjectTest.java @@ -61,7 +61,7 @@ public class ObjectTest extends TestCase { assertEquals(10, target.intValue); assertEquals(20, target.longValue); } - + public void testJsonInMixedQuotesDeserialization() { String json = "{\"stringValue\":'no message','intValue':10,'longValue':20}"; BagOfPrimitives target = gson.fromJson(json, BagOfPrimitives.class); @@ -69,7 +69,7 @@ public class ObjectTest extends TestCase { assertEquals(10, target.intValue); assertEquals(20, target.longValue); } - + public void testBagOfPrimitivesSerialization() throws Exception { BagOfPrimitives target = new BagOfPrimitives(10, 20, false, "stringValue"); assertEquals(target.getExpectedJson(), gson.toJson(target)); @@ -201,7 +201,7 @@ public class ObjectTest extends TestCase { String stringValue = "someStringValueInArray"; String classWithObjectsJson = gson.toJson(classWithObjects); String bagOfPrimitivesJson = gson.toJson(bagOfPrimitives); - + ClassWithArray classWithArray = new ClassWithArray( new Object[] { stringValue, classWithObjects, bagOfPrimitives }); String json = gson.toJson(classWithArray); @@ -267,7 +267,7 @@ public class ObjectTest extends TestCase { } public void testAnonymousLocalClassesSerialization() throws Exception { - assertEquals("", gson.toJson(new ClassWithNoFields() { + assertEquals("null", gson.toJson(new ClassWithNoFields() { // empty anonymous class })); } @@ -278,7 +278,7 @@ public class ObjectTest extends TestCase { } /** - * Tests that a class field with type Object can be serialized properly. + * Tests that a class field with type Object can be serialized properly. * See issue 54 */ public void testClassWithObjectFieldSerialization() { @@ -292,28 +292,28 @@ public class ObjectTest extends TestCase { @SuppressWarnings("unused") Object member; } - - public void testInnerClassSerialization() { + + public void testInnerClassSerialization() { Parent p = new Parent(); Parent.Child c = p.new Child(); String json = gson.toJson(c); assertTrue(json.contains("value2")); assertFalse(json.contains("value1")); } - + public void testInnerClassDeserialization() { final Parent p = new Parent(); Gson gson = new GsonBuilder().registerTypeAdapter( Parent.Child.class, new InstanceCreator() { public Parent.Child createInstance(Type type) { return p.new Child(); - } + } }).create(); String json = "{'value2':3}"; Parent.Child c = gson.fromJson(json, Parent.Child.class); assertEquals(3, c.value2); } - + private static class Parent { @SuppressWarnings("unused") int value1 = 1; @@ -365,7 +365,7 @@ public class ObjectTest extends TestCase { a = 10; } } - + /** * In response to Issue 41 http://code.google.com/p/google-gson/issues/detail?id=41 */ @@ -376,16 +376,16 @@ public class ObjectTest extends TestCase { assertTrue(bag.booleanValue); assertEquals("bar", bag.stringValue); } - + public void testStringFieldWithNumberValueDeserialization() { String json = "{\"stringValue\":1}"; BagOfPrimitives bag = gson.fromJson(json, BagOfPrimitives.class); assertEquals("1", bag.stringValue); - + json = "{\"stringValue\":1.5E+6}"; bag = gson.fromJson(json, BagOfPrimitives.class); assertEquals("1.5E+6", bag.stringValue); - + json = "{\"stringValue\":true}"; bag = gson.fromJson(json, BagOfPrimitives.class); assertEquals("true", bag.stringValue); @@ -419,7 +419,7 @@ public class ObjectTest extends TestCase { String b = ""; String c = ""; } - + public void testJsonObjectSerialization() { Gson gson = new GsonBuilder().serializeNulls().create(); JsonObject obj = new JsonObject(); diff --git a/gson/src/test/java/com/google/gson/functional/VersioningTest.java b/gson/src/test/java/com/google/gson/functional/VersioningTest.java index 90c1debb..81ccf3ef 100644 --- a/gson/src/test/java/com/google/gson/functional/VersioningTest.java +++ b/gson/src/test/java/com/google/gson/functional/VersioningTest.java @@ -48,12 +48,12 @@ public class VersioningTest extends TestCase { Gson gson = builder.setVersion(1.29).create(); String json = gson.toJson(target); assertTrue(json.contains("\"a\":" + A)); - + gson = builder.setVersion(1.3).create(); json = gson.toJson(target); assertFalse(json.contains("\"a\":" + A)); } - + public void testVersionedUntilDeserialization() { Gson gson = builder.setVersion(1.3).create(); String json = "{\"a\":3,\"b\":4,\"c\":5}"; @@ -82,7 +82,7 @@ public class VersioningTest extends TestCase { public void testIgnoreLaterVersionClassSerialization() { Gson gson = builder.setVersion(1.0).create(); - assertEquals("", gson.toJson(new Version1_2())); + assertEquals("null", gson.toJson(new Version1_2())); } public void testIgnoreLaterVersionClassDeserialization() { @@ -117,11 +117,11 @@ public class VersioningTest extends TestCase { SinceUntilMixing target = new SinceUntilMixing(); String json = gson.toJson(target); assertFalse(json.contains("\"b\":" + B)); - + gson = builder.setVersion(1.2).create(); json = gson.toJson(target); assertTrue(json.contains("\"b\":" + B)); - + gson = builder.setVersion(1.3).create(); json = gson.toJson(target); assertFalse(json.contains("\"b\":" + B)); @@ -133,12 +133,12 @@ public class VersioningTest extends TestCase { SinceUntilMixing result = gson.fromJson(json, SinceUntilMixing.class); assertEquals(5, result.a); assertEquals(B, result.b); - + gson = builder.setVersion(1.2).create(); result = gson.fromJson(json, SinceUntilMixing.class); assertEquals(5, result.a); assertEquals(6, result.b); - + gson = builder.setVersion(1.3).create(); result = gson.fromJson(json, SinceUntilMixing.class); assertEquals(5, result.a); @@ -158,10 +158,10 @@ public class VersioningTest extends TestCase { private static class Version1_2 extends Version1_1 { int d = D; } - + private static class SinceUntilMixing { int a = A; - + @Since(1.1) @Until(1.3) int b = B; diff --git a/gson/src/test/java/com/google/gson/internal/bind/JsonElementWriterTest.java b/gson/src/test/java/com/google/gson/internal/bind/JsonElementWriterTest.java index 27931938..4f37552a 100644 --- a/gson/src/test/java/com/google/gson/internal/bind/JsonElementWriterTest.java +++ b/gson/src/test/java/com/google/gson/internal/bind/JsonElementWriterTest.java @@ -24,6 +24,7 @@ public final class JsonElementWriterTest extends TestCase { // TODO: more tests // TODO: close support // TODO: figure out what should be returned by an empty writer + // TODO: test when serialize nulls is false public void testArray() throws IOException { JsonElementWriter writer = new JsonElementWriter();