New JsonAdapter.serializeNulls() method.

This makes it possible to force nulls into the document without
much fuss.
This commit is contained in:
jwilson
2017-01-29 22:18:12 -05:00
parent 99479682ba
commit b338d1e7ed
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 { @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); throw new IllegalArgumentException("Numeric values must be finite, but was " + value);
} }
if (promoteNameToValue) { 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 * Returns a JSON adapter equal to this JSON adapter, but with support for reading and writing
* nulls. * 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 java.util.List;
import okio.Buffer; import okio.Buffer;
abstract class JsonWriterFactory { abstract class JsonCodecFactory {
private static final Moshi MOSHI = new Moshi.Builder().build(); private static final Moshi MOSHI = new Moshi.Builder().build();
private static final JsonAdapter<Object> OBJECT_ADAPTER = MOSHI.adapter(Object.class); private static final JsonAdapter<Object> OBJECT_ADAPTER = MOSHI.adapter(Object.class);
static List<Object[]> factories() { static List<Object[]> factories() {
final JsonWriterFactory bufferedSink = new JsonWriterFactory() { final JsonCodecFactory bufferedSink = new JsonCodecFactory() {
Buffer buffer; Buffer buffer;
@Override public JsonReader newReader(String json) {
Buffer buffer = new Buffer().writeUtf8(json);
return JsonReader.of(buffer);
}
@Override JsonWriter newWriter() { @Override JsonWriter newWriter() {
buffer = new Buffer(); buffer = new Buffer();
return new BufferedSinkJsonWriter(buffer); return new BufferedSinkJsonWriter(buffer);
@@ -39,18 +44,29 @@ abstract class JsonWriterFactory {
return result; return result;
} }
@Override boolean supportsMultipleTopLevelValuesInOneDocument() { @Override boolean encodesToBytes() {
return true; return true;
} }
@Override public String toString() { @Override public String toString() {
return "BufferedSinkJsonWriter"; return "Buffer";
} }
}; };
final JsonWriterFactory object = new JsonWriterFactory() { final JsonCodecFactory object = new JsonCodecFactory() {
ObjectJsonWriter writer; 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() { @Override JsonWriter newWriter() {
writer = new ObjectJsonWriter(); writer = new ObjectJsonWriter();
return writer; return writer;
@@ -62,6 +78,7 @@ abstract class JsonWriterFactory {
Buffer buffer = new Buffer(); Buffer buffer = new Buffer();
JsonWriter bufferedSinkWriter = JsonWriter.of(buffer); JsonWriter bufferedSinkWriter = JsonWriter.of(buffer);
bufferedSinkWriter.setSerializeNulls(true); bufferedSinkWriter.setSerializeNulls(true);
bufferedSinkWriter.setLenient(true);
OBJECT_ADAPTER.toJson(bufferedSinkWriter, writer.root()); OBJECT_ADAPTER.toJson(bufferedSinkWriter, writer.root());
return buffer.readUtf8(); return buffer.readUtf8();
} catch (IOException e) { } catch (IOException e) {
@@ -75,7 +92,7 @@ abstract class JsonWriterFactory {
} }
@Override public String toString() { @Override public String toString() {
return "ObjectJsonWriter"; return "Object";
} }
}; };
@@ -84,10 +101,17 @@ abstract class JsonWriterFactory {
new Object[] { object }); new Object[] { object });
} }
abstract JsonReader newReader(String json) throws IOException;
abstract JsonWriter newWriter(); abstract JsonWriter newWriter();
boolean implementsStrictPrecision() {
return true;
}
abstract String json(); abstract String json();
boolean supportsMultipleTopLevelValuesInOneDocument() { boolean encodesToBytes() {
return false; 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) @RunWith(Parameterized.class)
public final class JsonReaderPathTest { public final class JsonReaderPathTest {
@Parameter public JsonReaderFactory factory; @Parameter public JsonCodecFactory factory;
@Parameters(name = "{0}") @Parameters(name = "{0}")
public static List<Object[]> parameters() { public static List<Object[]> parameters() {
return JsonReaderFactory.factories(); return JsonCodecFactory.factories();
} }
@Test public void path() throws IOException { @Test public void path() throws IOException {
@@ -185,7 +185,7 @@ public final class JsonReaderPathTest {
} }
@Test public void multipleTopLevelValuesInOneDocument() throws IOException { @Test public void multipleTopLevelValuesInOneDocument() throws IOException {
assumeTrue(factory.supportsMultipleTopLevelValuesInOneDocument()); assumeTrue(factory.encodesToBytes());
JsonReader reader = factory.newReader("[][]"); JsonReader reader = factory.newReader("[][]");
reader.setLenient(true); 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.BEGIN_OBJECT;
import static com.squareup.moshi.JsonReader.Token.NAME; import static com.squareup.moshi.JsonReader.Token.NAME;
import static com.squareup.moshi.JsonReader.Token.STRING; import static com.squareup.moshi.JsonReader.Token.STRING;
import static com.squareup.moshi.TestUtil.newReader;
import static com.squareup.moshi.TestUtil.repeat; import static com.squareup.moshi.TestUtil.repeat;
import static org.assertj.core.api.Assertions.assertThat; import static org.assertj.core.api.Assertions.assertThat;
import static org.junit.Assert.assertEquals; import static org.junit.Assert.assertEquals;
@@ -37,11 +36,11 @@ import static org.junit.Assume.assumeTrue;
@RunWith(Parameterized.class) @RunWith(Parameterized.class)
public final class JsonReaderTest { public final class JsonReaderTest {
@Parameter public JsonReaderFactory factory; @Parameter public JsonCodecFactory factory;
@Parameters(name = "{0}") @Parameters(name = "{0}")
public static List<Object[]> parameters() { public static List<Object[]> parameters() {
return JsonReaderFactory.factories(); return JsonCodecFactory.factories();
} }
JsonReader newReader(String json) throws IOException { JsonReader newReader(String json) throws IOException {

View File

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

View File

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