mirror of
https://github.com/fankes/moshi.git
synced 2025-10-20 00:19: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() {
|
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
|
||||||
|
@@ -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;
|
||||||
|
@@ -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" +
|
||||||
|
@@ -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;
|
||||||
|
Reference in New Issue
Block a user