mirror of
https://github.com/fankes/moshi.git
synced 2025-10-19 16:09:21 +08:00
Merge pull request #66 from square/jwilson__0802_forbid_skip
New APIs to reject unknown values.
This commit is contained in:
@@ -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() {
|
||||
final JsonAdapter<T> delegate = this;
|
||||
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 {
|
||||
/**
|
||||
* Attempts to create an adapter for {@code type} annotated with {@code annotations}. This
|
||||
|
@@ -214,6 +214,9 @@ public final class JsonReader implements Closeable {
|
||||
/** True to accept non-spec compliant JSON */
|
||||
private boolean lenient = false;
|
||||
|
||||
/** True to throw a {@link JsonDataException} on any attempt to call {@link #skipValue()}. */
|
||||
private boolean failOnUnknown = false;
|
||||
|
||||
/** The input JSON. */
|
||||
private final BufferedSource source;
|
||||
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
|
||||
* href="http://www.ietf.org/rfc/rfc4627.txt">RFC 4627</a>. Setting the
|
||||
* parser to lenient causes it to ignore the following syntax errors:
|
||||
@@ -302,6 +305,25 @@ public final class JsonReader implements Closeable {
|
||||
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
|
||||
* 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.
|
||||
* This method is intended for use when the JSON token stream contains unrecognized or unhandled
|
||||
* 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 {
|
||||
if (failOnUnknown) {
|
||||
throw new JsonDataException("Cannot skip unexpected " + peek() + " at " + getPath());
|
||||
}
|
||||
int count = 0;
|
||||
do {
|
||||
int p = peeked;
|
||||
|
@@ -19,7 +19,6 @@ import java.io.EOFException;
|
||||
import java.io.IOException;
|
||||
import java.util.Arrays;
|
||||
import okio.Buffer;
|
||||
import org.assertj.core.data.Offset;
|
||||
import org.junit.Ignore;
|
||||
import org.junit.Test;
|
||||
|
||||
@@ -191,6 +190,42 @@ public final class JsonReaderTest {
|
||||
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 {
|
||||
String json = "{\n" +
|
||||
" \"hello\": true,\n" +
|
||||
|
@@ -688,6 +688,25 @@ public final class MoshiTest {
|
||||
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 {
|
||||
final int diameter;
|
||||
final boolean extraCheese;
|
||||
|
Reference in New Issue
Block a user