Merge pull request #66 from square/jwilson__0802_forbid_skip

New APIs to reject unknown values.
This commit is contained in:
Jesse Wilson
2015-08-02 23:28:39 -04:00
4 changed files with 109 additions and 3 deletions

View File

@@ -78,7 +78,7 @@ public abstract class JsonAdapter<T> {
}; };
} }
/** Returns a JSON adapter equal to this JSON adapter, but is lenient when reading and writing. */ /** Returns a JSON adapter equal to this, but is lenient when reading and writing. */
public final JsonAdapter<T> lenient() { public final JsonAdapter<T> lenient() {
final JsonAdapter<T> delegate = this; final JsonAdapter<T> delegate = this;
return new JsonAdapter<T>() { return new JsonAdapter<T>() {
@@ -103,6 +103,30 @@ public abstract class JsonAdapter<T> {
}; };
} }
/**
* Returns a JSON adapter equal to this, but that throws a {@link JsonDataException} when
* {@linkplain JsonReader#setFailOnUnknown(boolean) unknown values} are encountered. This
* constraint applies to both the top-level message handled by this type adapter as well as to
* nested messages.
*/
public final JsonAdapter<T> failOnUnknown() {
final JsonAdapter<T> delegate = this;
return new JsonAdapter<T>() {
@Override public T fromJson(JsonReader reader) throws IOException {
boolean skipForbidden = reader.failOnUnknown();
reader.setFailOnUnknown(true);
try {
return delegate.fromJson(reader);
} finally {
reader.setFailOnUnknown(skipForbidden);
}
}
@Override public void toJson(JsonWriter writer, T value) throws IOException {
delegate.toJson(writer, value);
}
};
}
public interface Factory { public interface Factory {
/** /**
* Attempts to create an adapter for {@code type} annotated with {@code annotations}. This * Attempts to create an adapter for {@code type} annotated with {@code annotations}. This

View File

@@ -214,6 +214,9 @@ public final class JsonReader implements Closeable {
/** True to accept non-spec compliant JSON */ /** True to accept non-spec compliant JSON */
private boolean lenient = false; private boolean lenient = false;
/** True to throw a {@link JsonDataException} on any attempt to call {@link #skipValue()}. */
private boolean failOnUnknown = false;
/** The input JSON. */ /** The input JSON. */
private final BufferedSource source; private final BufferedSource source;
private final Buffer buffer; private final Buffer buffer;
@@ -263,7 +266,7 @@ public final class JsonReader implements Closeable {
} }
/** /**
* Configure this parser to be be liberal in what it accepts. By default, * Configure this parser to be liberal in what it accepts. By default
* this parser is strict and only accepts JSON as specified by <a * this parser is strict and only accepts JSON as specified by <a
* href="http://www.ietf.org/rfc/rfc4627.txt">RFC 4627</a>. Setting the * href="http://www.ietf.org/rfc/rfc4627.txt">RFC 4627</a>. Setting the
* parser to lenient causes it to ignore the following syntax errors: * parser to lenient causes it to ignore the following syntax errors:
@@ -302,6 +305,25 @@ public final class JsonReader implements Closeable {
return lenient; return lenient;
} }
/**
* Configure whether this parser throws a {@link JsonDataException} when {@link #skipValue} is
* called. By default this parser permits values to be skipped.
*
* <p>Forbid skipping to prevent unrecognized values from being silently ignored. This option is
* useful in development and debugging because it means a typo like "locatiom" will be detected
* early. It's potentially harmful in production because it complicates revising a JSON schema.
*/
public void setFailOnUnknown(boolean failOnUnknown) {
this.failOnUnknown = failOnUnknown;
}
/**
* Returns true if this parser forbids skipping values.
*/
public boolean failOnUnknown() {
return failOnUnknown;
}
/** /**
* Consumes the next token from the JSON stream and asserts that it is the beginning of a new * Consumes the next token from the JSON stream and asserts that it is the beginning of a new
* array. * array.
@@ -1082,8 +1104,14 @@ public final class JsonReader implements Closeable {
* Skips the next value recursively. If it is an object or array, all nested elements are skipped. * Skips the next value recursively. If it is an object or array, all nested elements are skipped.
* This method is intended for use when the JSON token stream contains unrecognized or unhandled * This method is intended for use when the JSON token stream contains unrecognized or unhandled
* values. * values.
*
* <p>This throws a {@link JsonDataException} if this parser has been configured to {@linkplain
* #failOnUnknown fail on unknown} values.
*/ */
public void skipValue() throws IOException { public void skipValue() throws IOException {
if (failOnUnknown) {
throw new JsonDataException("Cannot skip unexpected " + peek() + " at " + getPath());
}
int count = 0; int count = 0;
do { do {
int p = peeked; int p = peeked;

View File

@@ -19,7 +19,6 @@ import java.io.EOFException;
import java.io.IOException; import java.io.IOException;
import java.util.Arrays; import java.util.Arrays;
import okio.Buffer; import okio.Buffer;
import org.assertj.core.data.Offset;
import org.junit.Ignore; import org.junit.Ignore;
import org.junit.Test; import org.junit.Test;
@@ -191,6 +190,42 @@ public final class JsonReaderTest {
assertThat(reader.peek()).isEqualTo(JsonReader.Token.END_DOCUMENT); assertThat(reader.peek()).isEqualTo(JsonReader.Token.END_DOCUMENT);
} }
@Test public void failOnUnknownFailsOnUnknownObjectValue() throws IOException {
JsonReader reader = newReader("{\"a\": 123}");
reader.setFailOnUnknown(true);
reader.beginObject();
assertThat(reader.nextName()).isEqualTo("a");
try {
reader.skipValue();
fail();
} catch (JsonDataException expected) {
assertThat(expected).hasMessage("Cannot skip unexpected NUMBER at $.a");
}
// Confirm that the reader is left in a consistent state after the exception.
reader.setFailOnUnknown(false);
assertThat(reader.nextInt()).isEqualTo(123);
reader.endObject();
assertThat(reader.peek()).isEqualTo(JsonReader.Token.END_DOCUMENT);
}
@Test public void failOnUnknownFailsOnUnknownArrayElement() throws IOException {
JsonReader reader = newReader("[\"a\", 123]");
reader.setFailOnUnknown(true);
reader.beginArray();
assertThat(reader.nextString()).isEqualTo("a");
try {
reader.skipValue();
fail();
} catch (JsonDataException expected) {
assertThat(expected).hasMessage("Cannot skip unexpected NUMBER at $[1]");
}
// Confirm that the reader is left in a consistent state after the exception.
reader.setFailOnUnknown(false);
assertThat(reader.nextInt()).isEqualTo(123);
reader.endArray();
assertThat(reader.peek()).isEqualTo(JsonReader.Token.END_DOCUMENT);
}
@Test public void helloWorld() throws IOException { @Test public void helloWorld() throws IOException {
String json = "{\n" + String json = "{\n" +
" \"hello\": true,\n" + " \"hello\": true,\n" +

View File

@@ -688,6 +688,25 @@ public final class MoshiTest {
assertThat(adapter.toJson(null)).isEqualTo("null"); assertThat(adapter.toJson(null)).isEqualTo("null");
} }
@Test public void byDefaultUnknownFieldsAreIgnored() throws Exception {
Moshi moshi = new Moshi.Builder().build();
JsonAdapter<Pizza> adapter = moshi.adapter(Pizza.class);
Pizza pizza = adapter.fromJson("{\"diameter\":5,\"crust\":\"thick\",\"extraCheese\":true}");
assertThat(pizza.diameter).isEqualTo(5);
assertThat(pizza.extraCheese).isEqualTo(true);
}
@Test public void failOnUnknownThrowsOnUnknownFields() throws Exception {
Moshi moshi = new Moshi.Builder().build();
JsonAdapter<Pizza> adapter = moshi.adapter(Pizza.class).failOnUnknown();
try {
adapter.fromJson("{\"diameter\":5,\"crust\":\"thick\",\"extraCheese\":true}");
fail();
} catch (JsonDataException expected) {
assertThat(expected).hasMessage("Cannot skip unexpected STRING at $.crust");
}
}
static class Pizza { static class Pizza {
final int diameter; final int diameter;
final boolean extraCheese; final boolean extraCheese;