Merge pull request #238 from square/jwilson.0129.serializeNulls

New JsonAdapter.serializeNulls() method.
This commit is contained in:
Jake Wharton 2017-01-29 22:25:39 -05:00 committed by GitHub
commit b4367899cd
9 changed files with 235 additions and 91 deletions

View file

@ -200,7 +200,7 @@ final class BufferedSinkJsonWriter extends JsonWriter {
}
@Override public JsonWriter value(double value) throws IOException {
if (Double.isNaN(value) || Double.isInfinite(value)) {
if (!lenient && (Double.isNaN(value) || Double.isInfinite(value))) {
throw new IllegalArgumentException("Numeric values must be finite, but was " + value);
}
if (promoteNameToValue) {

View file

@ -83,6 +83,31 @@ public abstract class JsonAdapter<T> {
}
}
/**
* Returns a JSON adapter equal to this JSON adapter, but that serializes nulls when encoding
* JSON.
*/
public final JsonAdapter<T> serializeNulls() {
final JsonAdapter<T> delegate = this;
return new JsonAdapter<T>() {
@Override public T fromJson(JsonReader reader) throws IOException {
return delegate.fromJson(reader);
}
@Override public void toJson(JsonWriter writer, T value) throws IOException {
boolean serializeNulls = writer.getSerializeNulls();
writer.setSerializeNulls(true);
try {
delegate.toJson(writer, value);
} finally {
writer.setSerializeNulls(serializeNulls);
}
}
@Override public String toString() {
return delegate + ".serializeNulls()";
}
};
}
/**
* Returns a JSON adapter equal to this JSON adapter, but with support for reading and writing
* nulls.

View file

@ -0,0 +1,167 @@
/*
* Copyright (C) 2017 Square, 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.squareup.moshi;
import java.io.IOException;
import java.util.Arrays;
import java.util.Collections;
import java.util.List;
import java.util.Locale;
import java.util.Map;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.junit.runners.Parameterized;
import org.junit.runners.Parameterized.Parameter;
import org.junit.runners.Parameterized.Parameters;
import static org.assertj.core.api.Assertions.assertThat;
import static org.junit.Assert.fail;
import static org.junit.Assume.assumeTrue;
@RunWith(Parameterized.class)
public final class JsonAdapterTest {
@Parameter public JsonCodecFactory factory;
@Parameters(name = "{0}")
public static List<Object[]> parameters() {
return JsonCodecFactory.factories();
}
@Test public void lenient() throws Exception {
JsonAdapter<Double> lenient = new JsonAdapter<Double>() {
@Override public Double fromJson(JsonReader reader) throws IOException {
return reader.nextDouble();
}
@Override public void toJson(JsonWriter writer, Double value) throws IOException {
writer.value(value);
}
}.lenient();
JsonReader reader = factory.newReader("[-Infinity, NaN, Infinity]");
reader.beginArray();
assertThat(lenient.fromJson(reader)).isEqualTo(Double.NEGATIVE_INFINITY);
assertThat(lenient.fromJson(reader)).isNaN();
assertThat(lenient.fromJson(reader)).isEqualTo(Double.POSITIVE_INFINITY);
reader.endArray();
JsonWriter writer = factory.newWriter();
writer.beginArray();
lenient.toJson(writer, Double.NEGATIVE_INFINITY);
lenient.toJson(writer, Double.NaN);
lenient.toJson(writer, Double.POSITIVE_INFINITY);
writer.endArray();
assertThat(factory.json()).isEqualTo("[-Infinity,NaN,Infinity]");
}
@Test public void nullSafe() throws Exception {
JsonAdapter<String> toUpperCase = new JsonAdapter<String>() {
@Override public String fromJson(JsonReader reader) throws IOException {
return reader.nextString().toUpperCase(Locale.US);
}
@Override public void toJson(JsonWriter writer, String value) throws IOException {
writer.value(value.toUpperCase(Locale.US));
}
}.nullSafe();
JsonReader reader = factory.newReader("[\"a\", null, \"c\"]");
reader.beginArray();
assertThat(toUpperCase.fromJson(reader)).isEqualTo("A");
assertThat(toUpperCase.fromJson(reader)).isNull();
assertThat(toUpperCase.fromJson(reader)).isEqualTo("C");
reader.endArray();
JsonWriter writer = factory.newWriter();
writer.beginArray();
toUpperCase.toJson(writer, "a");
toUpperCase.toJson(writer, null);
toUpperCase.toJson(writer, "c");
writer.endArray();
assertThat(factory.json()).isEqualTo("[\"A\",null,\"C\"]");
}
@Test public void failOnUnknown() throws Exception {
JsonAdapter<String> alwaysSkip = new JsonAdapter<String>() {
@Override public String fromJson(JsonReader reader) throws IOException {
reader.skipValue();
throw new AssertionError();
}
@Override public void toJson(JsonWriter writer, String value) throws IOException {
throw new AssertionError();
}
}.failOnUnknown();
JsonReader reader = factory.newReader("[\"a\"]");
reader.beginArray();
try {
alwaysSkip.fromJson(reader);
fail();
} catch (JsonDataException expected) {
assertThat(expected).hasMessage("Cannot skip unexpected STRING at $[0]");
}
assertThat(reader.nextString()).isEqualTo("a");
reader.endArray();
}
@Test public void indent() throws Exception {
assumeTrue(factory.encodesToBytes());
JsonAdapter<List<String>> indent = new JsonAdapter<List<String>>() {
@Override public List<String> fromJson(JsonReader reader) throws IOException {
throw new AssertionError();
}
@Override public void toJson(JsonWriter writer, List<String> value) throws IOException {
writer.beginArray();
for (String s : value) {
writer.value(s);
}
writer.endArray();
}
}.indent("\t\t\t");
JsonWriter writer = factory.newWriter();
indent.toJson(writer, Arrays.asList("a", "b", "c"));
assertThat(factory.json()).isEqualTo(""
+ "[\n"
+ "\t\t\t\"a\",\n"
+ "\t\t\t\"b\",\n"
+ "\t\t\t\"c\"\n"
+ "]");
}
@Test public void serializeNulls() throws Exception {
JsonAdapter<Map<String, String>> serializeNulls = new JsonAdapter<Map<String, String>>() {
@Override public Map<String, String> fromJson(JsonReader reader) throws IOException {
throw new AssertionError();
}
@Override public void toJson(JsonWriter writer, Map<String, String> map) throws IOException {
writer.beginObject();
for (Map.Entry<String, String> entry : map.entrySet()) {
writer.name(entry.getKey()).value(entry.getValue());
}
writer.endObject();
}
}.serializeNulls();
JsonWriter writer = factory.newWriter();
serializeNulls.toJson(writer, Collections.<String, String>singletonMap("a", null));
assertThat(factory.json()).isEqualTo("{\"a\":null}");
}
}

View file

@ -20,14 +20,19 @@ import java.util.Arrays;
import java.util.List;
import okio.Buffer;
abstract class JsonWriterFactory {
abstract class JsonCodecFactory {
private static final Moshi MOSHI = new Moshi.Builder().build();
private static final JsonAdapter<Object> OBJECT_ADAPTER = MOSHI.adapter(Object.class);
static List<Object[]> factories() {
final JsonWriterFactory bufferedSink = new JsonWriterFactory() {
final JsonCodecFactory bufferedSink = new JsonCodecFactory() {
Buffer buffer;
@Override public JsonReader newReader(String json) {
Buffer buffer = new Buffer().writeUtf8(json);
return JsonReader.of(buffer);
}
@Override JsonWriter newWriter() {
buffer = new Buffer();
return new BufferedSinkJsonWriter(buffer);
@ -39,18 +44,29 @@ abstract class JsonWriterFactory {
return result;
}
@Override boolean supportsMultipleTopLevelValuesInOneDocument() {
@Override boolean encodesToBytes() {
return true;
}
@Override public String toString() {
return "BufferedSinkJsonWriter";
return "Buffer";
}
};
final JsonWriterFactory object = new JsonWriterFactory() {
final JsonCodecFactory object = new JsonCodecFactory() {
ObjectJsonWriter writer;
@Override public JsonReader newReader(String json) throws IOException {
Moshi moshi = new Moshi.Builder().build();
Object object = moshi.adapter(Object.class).lenient().fromJson(json);
return new ObjectJsonReader(object);
}
// TODO(jwilson): fix precision checks and delete his method.
@Override boolean implementsStrictPrecision() {
return false;
}
@Override JsonWriter newWriter() {
writer = new ObjectJsonWriter();
return writer;
@ -62,6 +78,7 @@ abstract class JsonWriterFactory {
Buffer buffer = new Buffer();
JsonWriter bufferedSinkWriter = JsonWriter.of(buffer);
bufferedSinkWriter.setSerializeNulls(true);
bufferedSinkWriter.setLenient(true);
OBJECT_ADAPTER.toJson(bufferedSinkWriter, writer.root());
return buffer.readUtf8();
} catch (IOException e) {
@ -75,7 +92,7 @@ abstract class JsonWriterFactory {
}
@Override public String toString() {
return "ObjectJsonWriter";
return "Object";
}
};
@ -84,10 +101,17 @@ abstract class JsonWriterFactory {
new Object[] { object });
}
abstract JsonReader newReader(String json) throws IOException;
abstract JsonWriter newWriter();
boolean implementsStrictPrecision() {
return true;
}
abstract String json();
boolean supportsMultipleTopLevelValuesInOneDocument() {
boolean encodesToBytes() {
return false;
}

View file

@ -1,71 +0,0 @@
/*
* Copyright (C) 2017 Square, 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.squareup.moshi;
import java.io.IOException;
import java.util.Arrays;
import java.util.List;
import okio.Buffer;
abstract class JsonReaderFactory {
static List<Object[]> factories() {
JsonReaderFactory bufferedSource = new JsonReaderFactory() {
@Override public JsonReader newReader(String json) {
Buffer buffer = new Buffer().writeUtf8(json);
return JsonReader.of(buffer);
}
@Override boolean supportsMultipleTopLevelValuesInOneDocument() {
return true;
}
@Override public String toString() {
return "BufferedSourceJsonReader";
}
};
JsonReaderFactory jsonObject = new JsonReaderFactory() {
@Override public JsonReader newReader(String json) throws IOException {
Moshi moshi = new Moshi.Builder().build();
Object object = moshi.adapter(Object.class).lenient().fromJson(json);
return new ObjectJsonReader(object);
}
// TODO(jwilson): fix precision checks and delete his method.
@Override boolean implementsStrictPrecision() {
return false;
}
@Override public String toString() {
return "ObjectJsonReader";
}
};
return Arrays.asList(
new Object[] { bufferedSource },
new Object[] { jsonObject });
}
abstract JsonReader newReader(String json) throws IOException;
boolean supportsMultipleTopLevelValuesInOneDocument() {
return false;
}
boolean implementsStrictPrecision() {
return true;
}
}

View file

@ -28,11 +28,11 @@ import static org.junit.Assume.assumeTrue;
@RunWith(Parameterized.class)
public final class JsonReaderPathTest {
@Parameter public JsonReaderFactory factory;
@Parameter public JsonCodecFactory factory;
@Parameters(name = "{0}")
public static List<Object[]> parameters() {
return JsonReaderFactory.factories();
return JsonCodecFactory.factories();
}
@Test public void path() throws IOException {
@ -185,7 +185,7 @@ public final class JsonReaderPathTest {
}
@Test public void multipleTopLevelValuesInOneDocument() throws IOException {
assumeTrue(factory.supportsMultipleTopLevelValuesInOneDocument());
assumeTrue(factory.encodesToBytes());
JsonReader reader = factory.newReader("[][]");
reader.setLenient(true);

View file

@ -28,7 +28,6 @@ import static com.squareup.moshi.JsonReader.Token.BEGIN_ARRAY;
import static com.squareup.moshi.JsonReader.Token.BEGIN_OBJECT;
import static com.squareup.moshi.JsonReader.Token.NAME;
import static com.squareup.moshi.JsonReader.Token.STRING;
import static com.squareup.moshi.TestUtil.newReader;
import static com.squareup.moshi.TestUtil.repeat;
import static org.assertj.core.api.Assertions.assertThat;
import static org.junit.Assert.assertEquals;
@ -37,11 +36,11 @@ import static org.junit.Assume.assumeTrue;
@RunWith(Parameterized.class)
public final class JsonReaderTest {
@Parameter public JsonReaderFactory factory;
@Parameter public JsonCodecFactory factory;
@Parameters(name = "{0}")
public static List<Object[]> parameters() {
return JsonReaderFactory.factories();
return JsonCodecFactory.factories();
}
JsonReader newReader(String json) throws IOException {

View file

@ -29,11 +29,11 @@ import static org.junit.Assume.assumeTrue;
@RunWith(Parameterized.class)
public final class JsonWriterPathTest {
@Parameter public JsonWriterFactory factory;
@Parameter public JsonCodecFactory factory;
@Parameters(name = "{0}")
public static List<Object[]> parameters() {
return JsonWriterFactory.factories();
return JsonCodecFactory.factories();
}
@Test public void path() throws IOException {
@ -202,7 +202,7 @@ public final class JsonWriterPathTest {
}
@Test public void multipleTopLevelValuesInOneDocument() throws IOException {
assumeTrue(factory.supportsMultipleTopLevelValuesInOneDocument());
assumeTrue(factory.encodesToBytes());
JsonWriter writer = factory.newWriter();
writer.setLenient(true);

View file

@ -31,11 +31,11 @@ import static org.junit.Assume.assumeTrue;
@RunWith(Parameterized.class)
public final class JsonWriterTest {
@Parameter public JsonWriterFactory factory;
@Parameter public JsonCodecFactory factory;
@Parameters(name = "{0}")
public static List<Object[]> parameters() {
return JsonWriterFactory.factories();
return JsonCodecFactory.factories();
}
@Test public void nullsValuesNotSerializedByDefault() throws IOException {
@ -469,7 +469,7 @@ public final class JsonWriterTest {
}
@Test public void lenientWriterPermitsMultipleTopLevelValues() throws IOException {
assumeTrue(factory.supportsMultipleTopLevelValuesInOneDocument());
assumeTrue(factory.encodesToBytes());
JsonWriter writer = factory.newWriter();
writer.setLenient(true);