Merge pull request #327 from bdhess/allow-trailing-comma
Add JsonParser feature to ignore a trailing comma (fixes #118, #323)
diff --git a/src/main/java/com/fasterxml/jackson/core/JsonParser.java b/src/main/java/com/fasterxml/jackson/core/JsonParser.java
index 2fd0011..661cad8 100644
--- a/src/main/java/com/fasterxml/jackson/core/JsonParser.java
+++ b/src/main/java/com/fasterxml/jackson/core/JsonParser.java
@@ -227,7 +227,30 @@
*
* @since 2.8
*/
- ALLOW_MISSING_VALUES(false)
+ ALLOW_MISSING_VALUES(false),
+
+ /**
+ * Feature that determines whether {@link JsonParser} will allow for a single trailing
+ * comma following the final value (in an Array) or member (in an Object). These commas
+ * will simply be ignored.
+ * <p>
+ * For example, when this feature is enabled, <code>[true,true,]</code> is equivalent to
+ * <code>[true, true]</code> and <code>{"a": true,}</code> is equivalent to
+ * <code>{"a": true}</code>.
+ * <p>
+ * When combined with <code>ALLOW_MISSING_VALUES</code>, this feature takes priority, and
+ * the final trailing comma in an array declaration does not imply a missing
+ * (<code>null</code>) value. For example, when both <code>ALLOW_MISSING_VALUES</code>
+ * and <code>ALLOW_TRAILING_COMMA</code> are enabled, <code>[true,true,]</code> is
+ * equivalent to <code>[true, true]</code>, and <code>[true,true,,]</code> is equivalent to
+ * <code>[true, true, null]</code>.
+ * <p>
+ * Since the JSON specification does not permit trailing commas, this is a non-standard
+ * feature, and as such disabled by default.
+ *
+ * @since 2.9
+ */
+ ALLOW_TRAILING_COMMA(false)
;
/**
diff --git a/src/main/java/com/fasterxml/jackson/core/json/ReaderBasedJsonParser.java b/src/main/java/com/fasterxml/jackson/core/json/ReaderBasedJsonParser.java
index dc80f62..b27de2e 100644
--- a/src/main/java/com/fasterxml/jackson/core/json/ReaderBasedJsonParser.java
+++ b/src/main/java/com/fasterxml/jackson/core/json/ReaderBasedJsonParser.java
@@ -652,26 +652,20 @@
_binaryValue = null;
// Closing scope?
- if (i == INT_RBRACKET) {
- _updateLocation();
- if (!_parsingContext.inArray()) {
- _reportMismatchedEndMarker(i, '}');
- }
- _parsingContext = _parsingContext.clearAndGetParent();
- return (_currToken = JsonToken.END_ARRAY);
- }
- if (i == INT_RCURLY) {
- _updateLocation();
- if (!_parsingContext.inObject()) {
- _reportMismatchedEndMarker(i, ']');
- }
- _parsingContext = _parsingContext.clearAndGetParent();
- return (_currToken = JsonToken.END_OBJECT);
+ if (i == INT_RBRACKET || i == INT_RCURLY) {
+ _closeScope(i);
+ return _currToken;
}
// Nope: do we then expect a comma?
if (_parsingContext.expectComma()) {
i = _skipComma(i);
+
+ // Was that a trailing comma?
+ if (isEnabled(Feature.ALLOW_TRAILING_COMMA) && (i == INT_RBRACKET || i == INT_RCURLY)) {
+ _closeScope(i);
+ return _currToken;
+ }
}
/* And should we now have a name? Always true for Object contexts, since
@@ -811,26 +805,20 @@
}
_binaryValue = null;
- if (i == INT_RBRACKET) {
- _updateLocation();
- if (!_parsingContext.inArray()) {
- _reportMismatchedEndMarker(i, '}');
- }
- _parsingContext = _parsingContext.clearAndGetParent();
- _currToken = JsonToken.END_ARRAY;
+ // Closing scope?
+ if (i == INT_RBRACKET || i == INT_RCURLY) {
+ _closeScope(i);
return false;
}
- if (i == INT_RCURLY) {
- _updateLocation();
- if (!_parsingContext.inObject()) {
- _reportMismatchedEndMarker(i, ']');
- }
- _parsingContext = _parsingContext.clearAndGetParent();
- _currToken = JsonToken.END_OBJECT;
- return false;
- }
+
if (_parsingContext.expectComma()) {
i = _skipComma(i);
+
+ // Was that a trailing comma?
+ if (isEnabled(Feature.ALLOW_TRAILING_COMMA) && (i == INT_RBRACKET || i == INT_RCURLY)) {
+ _closeScope(i);
+ return false;
+ }
}
if (!_parsingContext.inObject()) {
@@ -2834,4 +2822,29 @@
}
_reportError("Unrecognized token '"+sb.toString()+"': was expecting "+msg);
}
+
+ /*
+ /**********************************************************
+ /* Internal methods, other
+ /**********************************************************
+ */
+
+ private void _closeScope(int i) throws JsonParseException {
+ if (i == INT_RBRACKET) {
+ _updateLocation();
+ if (!_parsingContext.inArray()) {
+ _reportMismatchedEndMarker(i, '}');
+ }
+ _parsingContext = _parsingContext.clearAndGetParent();
+ _currToken = JsonToken.END_ARRAY;
+ }
+ if (i == INT_RCURLY) {
+ _updateLocation();
+ if (!_parsingContext.inObject()) {
+ _reportMismatchedEndMarker(i, ']');
+ }
+ _parsingContext = _parsingContext.clearAndGetParent();
+ _currToken = JsonToken.END_OBJECT;
+ }
+ }
}
diff --git a/src/main/java/com/fasterxml/jackson/core/json/UTF8DataInputJsonParser.java b/src/main/java/com/fasterxml/jackson/core/json/UTF8DataInputJsonParser.java
index 8bc3789..de5d081 100644
--- a/src/main/java/com/fasterxml/jackson/core/json/UTF8DataInputJsonParser.java
+++ b/src/main/java/com/fasterxml/jackson/core/json/UTF8DataInputJsonParser.java
@@ -575,19 +575,9 @@
_tokenInputRow = _currInputRow;
// Closing scope?
- if (i == INT_RBRACKET) {
- if (!_parsingContext.inArray()) {
- _reportMismatchedEndMarker(i, '}');
- }
- _parsingContext = _parsingContext.clearAndGetParent();
- return (_currToken = JsonToken.END_ARRAY);
- }
- if (i == INT_RCURLY) {
- if (!_parsingContext.inObject()) {
- _reportMismatchedEndMarker(i, ']');
- }
- _parsingContext = _parsingContext.clearAndGetParent();
- return (_currToken = JsonToken.END_OBJECT);
+ if (i == INT_RBRACKET || i == INT_RCURLY) {
+ _closeScope(i);
+ return _currToken;
}
// Nope: do we then expect a comma?
@@ -596,6 +586,12 @@
_reportUnexpectedChar(i, "was expecting comma to separate "+_parsingContext.typeDesc()+" entries");
}
i = _skipWS();
+
+ // Was that a trailing comma?
+ if (isEnabled(Feature.ALLOW_TRAILING_COMMA) && (i == INT_RBRACKET || i == INT_RCURLY)) {
+ _closeScope(i);
+ return _currToken;
+ }
}
/* And should we now have a name? Always true for
@@ -2788,6 +2784,23 @@
/**********************************************************
*/
+ private void _closeScope(int i) throws JsonParseException {
+ if (i == INT_RBRACKET) {
+ if (!_parsingContext.inArray()) {
+ _reportMismatchedEndMarker(i, '}');
+ }
+ _parsingContext = _parsingContext.clearAndGetParent();
+ _currToken = JsonToken.END_ARRAY;
+ }
+ if (i == INT_RCURLY) {
+ if (!_parsingContext.inObject()) {
+ _reportMismatchedEndMarker(i, ']');
+ }
+ _parsingContext = _parsingContext.clearAndGetParent();
+ _currToken = JsonToken.END_OBJECT;
+ }
+ }
+
/**
* Helper method needed to fix [Issue#148], masking of 0x00 character
*/
diff --git a/src/main/java/com/fasterxml/jackson/core/json/UTF8StreamJsonParser.java b/src/main/java/com/fasterxml/jackson/core/json/UTF8StreamJsonParser.java
index 5a0dcda..23f52bf 100644
--- a/src/main/java/com/fasterxml/jackson/core/json/UTF8StreamJsonParser.java
+++ b/src/main/java/com/fasterxml/jackson/core/json/UTF8StreamJsonParser.java
@@ -738,21 +738,9 @@
_binaryValue = null;
// Closing scope?
- if (i == INT_RBRACKET) {
- _updateLocation();
- if (!_parsingContext.inArray()) {
- _reportMismatchedEndMarker(i, '}');
- }
- _parsingContext = _parsingContext.clearAndGetParent();
- return (_currToken = JsonToken.END_ARRAY);
- }
- if (i == INT_RCURLY) {
- _updateLocation();
- if (!_parsingContext.inObject()) {
- _reportMismatchedEndMarker(i, ']');
- }
- _parsingContext = _parsingContext.clearAndGetParent();
- return (_currToken = JsonToken.END_OBJECT);
+ if (i == INT_RBRACKET || i == INT_RCURLY) {
+ _closeScope(i);
+ return _currToken;
}
// Nope: do we then expect a comma?
@@ -761,6 +749,12 @@
_reportUnexpectedChar(i, "was expecting comma to separate "+_parsingContext.typeDesc()+" entries");
}
i = _skipWS();
+
+ // Was that a trailing comma?
+ if (isEnabled(Feature.ALLOW_TRAILING_COMMA) && (i == INT_RBRACKET || i == INT_RCURLY)) {
+ _closeScope(i);
+ return _currToken;
+ }
}
/* And should we now have a name? Always true for
@@ -930,22 +924,8 @@
_binaryValue = null;
// Closing scope?
- if (i == INT_RBRACKET) {
- _updateLocation();
- if (!_parsingContext.inArray()) {
- _reportMismatchedEndMarker(i, '}');
- }
- _parsingContext = _parsingContext.clearAndGetParent();
- _currToken = JsonToken.END_ARRAY;
- return false;
- }
- if (i == INT_RCURLY) {
- _updateLocation();
- if (!_parsingContext.inObject()) {
- _reportMismatchedEndMarker(i, ']');
- }
- _parsingContext = _parsingContext.clearAndGetParent();
- _currToken = JsonToken.END_OBJECT;
+ if (i == INT_RBRACKET || i == INT_RCURLY) {
+ _closeScope(i);
return false;
}
@@ -955,6 +935,12 @@
_reportUnexpectedChar(i, "was expecting comma to separate "+_parsingContext.typeDesc()+" entries");
}
i = _skipWS();
+
+ // Was that a trailing comma?
+ if (isEnabled(Feature.ALLOW_TRAILING_COMMA) && (i == INT_RBRACKET || i == INT_RCURLY)) {
+ _closeScope(i);
+ return false;
+ }
}
if (!_parsingContext.inObject()) {
@@ -1017,22 +1003,8 @@
}
_binaryValue = null;
- if (i == INT_RBRACKET) {
- _updateLocation();
- if (!_parsingContext.inArray()) {
- _reportMismatchedEndMarker(i, '}');
- }
- _parsingContext = _parsingContext.clearAndGetParent();
- _currToken = JsonToken.END_ARRAY;
- return null;
- }
- if (i == INT_RCURLY) {
- _updateLocation();
- if (!_parsingContext.inObject()) {
- _reportMismatchedEndMarker(i, ']');
- }
- _parsingContext = _parsingContext.clearAndGetParent();
- _currToken = JsonToken.END_OBJECT;
+ if (i == INT_RBRACKET || i == INT_RCURLY) {
+ _closeScope(i);
return null;
}
@@ -1042,7 +1014,14 @@
_reportUnexpectedChar(i, "was expecting comma to separate "+_parsingContext.typeDesc()+" entries");
}
i = _skipWS();
+
+ // Was that a trailing comma?
+ if (isEnabled(Feature.ALLOW_TRAILING_COMMA) && (i == INT_RBRACKET || i == INT_RCURLY)) {
+ _closeScope(i);
+ return null;
+ }
}
+
if (!_parsingContext.inObject()) {
_updateLocation();
_nextTokenNotInObject(i);
@@ -3733,6 +3712,25 @@
/**********************************************************
*/
+ private void _closeScope(int i) throws JsonParseException {
+ if (i == INT_RBRACKET) {
+ _updateLocation();
+ if (!_parsingContext.inArray()) {
+ _reportMismatchedEndMarker(i, '}');
+ }
+ _parsingContext = _parsingContext.clearAndGetParent();
+ _currToken = JsonToken.END_ARRAY;
+ }
+ if (i == INT_RCURLY) {
+ _updateLocation();
+ if (!_parsingContext.inObject()) {
+ _reportMismatchedEndMarker(i, ']');
+ }
+ _parsingContext = _parsingContext.clearAndGetParent();
+ _currToken = JsonToken.END_OBJECT;
+ }
+ }
+
/**
* Helper method needed to fix [Issue#148], masking of 0x00 character
*/
diff --git a/src/test/java/com/fasterxml/jackson/core/read/TrailingCommasTest.java b/src/test/java/com/fasterxml/jackson/core/read/TrailingCommasTest.java
new file mode 100644
index 0000000..972b650
--- /dev/null
+++ b/src/test/java/com/fasterxml/jackson/core/read/TrailingCommasTest.java
@@ -0,0 +1,316 @@
+package com.fasterxml.jackson.core.read;
+
+import com.fasterxml.jackson.core.BaseTest;
+import com.fasterxml.jackson.core.JsonFactory;
+import com.fasterxml.jackson.core.JsonParser;
+import com.fasterxml.jackson.core.JsonParser.Feature;
+import com.fasterxml.jackson.core.JsonToken;
+import com.fasterxml.jackson.core.json.UTF8DataInputJsonParser;
+
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.junit.runners.Parameterized;
+
+import java.io.IOException;
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.Collection;
+import java.util.Collections;
+import java.util.HashSet;
+import java.util.List;
+
+@RunWith(Parameterized.class)
+public class TrailingCommasTest extends BaseTest {
+
+ private final JsonFactory factory;
+ private final HashSet<JsonParser.Feature> features;
+ private final int mode;
+
+ public TrailingCommasTest(int mode, List<Feature> features) {
+ this.factory = new JsonFactory();
+ this.features = new HashSet<JsonParser.Feature>(features);
+
+ for (JsonParser.Feature feature : features) {
+ factory.enable(feature);
+ }
+
+ this.mode = mode;
+ }
+
+ @Parameterized.Parameters(name = "Mode {0}, Features {1}")
+ public static Collection<Object[]> getTestCases() {
+ ArrayList<Object[]> cases = new ArrayList<Object[]>();
+
+ for (int mode : ALL_MODES) {
+ cases.add(new Object[]{mode, Collections.emptyList()});
+ cases.add(new Object[]{mode, Arrays.asList(Feature.ALLOW_MISSING_VALUES)});
+ cases.add(new Object[]{mode, Arrays.asList(Feature.ALLOW_TRAILING_COMMA)});
+ cases.add(new Object[]{mode, Arrays.asList(Feature.ALLOW_MISSING_VALUES, Feature.ALLOW_TRAILING_COMMA)});
+ }
+
+ return cases;
+ }
+
+ @Test
+ public void testArrayBasic() throws Exception {
+ String json = "[\"a\", \"b\"]";
+
+ JsonParser p = createParser(factory, mode, json);
+
+ assertEquals(JsonToken.START_ARRAY, p.nextToken());
+
+ assertToken(JsonToken.VALUE_STRING, p.nextToken());
+ assertEquals("a", p.getText());
+
+ assertToken(JsonToken.VALUE_STRING, p.nextToken());
+ assertEquals("b", p.getText());
+
+ assertEquals(JsonToken.END_ARRAY, p.nextToken());
+ assertEnd(p);
+ }
+
+ @Test
+ public void testArrayInnerComma() throws Exception {
+ String json = "[\"a\",, \"b\"]";
+
+ JsonParser p = createParser(factory, mode, json);
+
+ assertEquals(JsonToken.START_ARRAY, p.nextToken());
+
+ assertToken(JsonToken.VALUE_STRING, p.nextToken());
+ assertEquals("a", p.getText());
+
+ if (!features.contains(Feature.ALLOW_MISSING_VALUES)) {
+ assertUnexpected(p, ',');
+ return;
+ }
+
+ assertToken(JsonToken.VALUE_NULL, p.nextToken());
+
+ assertToken(JsonToken.VALUE_STRING, p.nextToken());
+ assertEquals("b", p.getText());
+
+ assertEquals(JsonToken.END_ARRAY, p.nextToken());
+ assertEnd(p);
+ }
+
+ @Test
+ public void testArrayLeadingComma() throws Exception {
+ String json = "[,\"a\", \"b\"]";
+
+ JsonParser p = createParser(factory, mode, json);
+
+ assertEquals(JsonToken.START_ARRAY, p.nextToken());
+
+ if (!features.contains(Feature.ALLOW_MISSING_VALUES)) {
+ assertUnexpected(p, ',');
+ return;
+ }
+
+ assertToken(JsonToken.VALUE_NULL, p.nextToken());
+
+ assertToken(JsonToken.VALUE_STRING, p.nextToken());
+ assertEquals("a", p.getText());
+
+ assertToken(JsonToken.VALUE_STRING, p.nextToken());
+ assertEquals("b", p.getText());
+
+ assertEquals(JsonToken.END_ARRAY, p.nextToken());
+ assertEnd(p);
+ }
+
+ @Test
+ public void testArrayTrailingComma() throws Exception {
+ String json = "[\"a\", \"b\",]";
+
+ JsonParser p = createParser(factory, mode, json);
+
+ assertEquals(JsonToken.START_ARRAY, p.nextToken());
+
+ assertToken(JsonToken.VALUE_STRING, p.nextToken());
+ assertEquals("a", p.getText());
+
+ assertToken(JsonToken.VALUE_STRING, p.nextToken());
+ assertEquals("b", p.getText());
+
+ // ALLOW_TRAILING_COMMA takes priority over ALLOW_MISSING_VALUES
+ if (features.contains(Feature.ALLOW_TRAILING_COMMA)) {
+ assertToken(JsonToken.END_ARRAY, p.nextToken());
+ assertEnd(p);
+ } else if (features.contains(Feature.ALLOW_MISSING_VALUES)) {
+ assertToken(JsonToken.VALUE_NULL, p.nextToken());
+ assertToken(JsonToken.END_ARRAY, p.nextToken());
+ assertEnd(p);
+ } else {
+ assertUnexpected(p, ']');
+ }
+ }
+
+ @Test
+ public void testArrayTrailingCommas() throws Exception {
+ String json = "[\"a\", \"b\",,]";
+
+ JsonParser p = createParser(factory, mode, json);
+
+ assertEquals(JsonToken.START_ARRAY, p.nextToken());
+
+ assertToken(JsonToken.VALUE_STRING, p.nextToken());
+ assertEquals("a", p.getText());
+
+ assertToken(JsonToken.VALUE_STRING, p.nextToken());
+ assertEquals("b", p.getText());
+
+ // ALLOW_TRAILING_COMMA takes priority over ALLOW_MISSING_VALUES
+ if (features.contains(Feature.ALLOW_MISSING_VALUES) &&
+ features.contains(Feature.ALLOW_TRAILING_COMMA)) {
+ assertToken(JsonToken.VALUE_NULL, p.nextToken());
+ assertToken(JsonToken.END_ARRAY, p.nextToken());
+ assertEnd(p);
+ } else if (features.contains(Feature.ALLOW_MISSING_VALUES)) {
+ assertToken(JsonToken.VALUE_NULL, p.nextToken());
+ assertToken(JsonToken.VALUE_NULL, p.nextToken());
+ assertToken(JsonToken.END_ARRAY, p.nextToken());
+ assertEnd(p);
+ } else {
+ assertUnexpected(p, ',');
+ }
+ }
+
+ @Test
+ public void testArrayTrailingCommasTriple() throws Exception {
+ String json = "[\"a\", \"b\",,,]";
+
+ JsonParser p = createParser(factory, mode, json);
+
+ assertEquals(JsonToken.START_ARRAY, p.nextToken());
+
+ assertToken(JsonToken.VALUE_STRING, p.nextToken());
+ assertEquals("a", p.getText());
+
+ assertToken(JsonToken.VALUE_STRING, p.nextToken());
+ assertEquals("b", p.getText());
+
+ // ALLOW_TRAILING_COMMA takes priority over ALLOW_MISSING_VALUES
+ if (features.contains(Feature.ALLOW_MISSING_VALUES) &&
+ features.contains(Feature.ALLOW_TRAILING_COMMA)) {
+ assertToken(JsonToken.VALUE_NULL, p.nextToken());
+ assertToken(JsonToken.VALUE_NULL, p.nextToken());
+ assertToken(JsonToken.END_ARRAY, p.nextToken());
+ assertEnd(p);
+ } else if (features.contains(Feature.ALLOW_MISSING_VALUES)) {
+ assertToken(JsonToken.VALUE_NULL, p.nextToken());
+ assertToken(JsonToken.VALUE_NULL, p.nextToken());
+ assertToken(JsonToken.VALUE_NULL, p.nextToken());
+ assertToken(JsonToken.END_ARRAY, p.nextToken());
+ assertEnd(p);
+ } else {
+ assertUnexpected(p, ',');
+ }
+ }
+
+ @Test
+ public void testObjectBasic() throws Exception {
+ String json = "{\"a\": true, \"b\": false}";
+
+ JsonParser p = createParser(factory, mode, json);
+
+ assertEquals(JsonToken.START_OBJECT, p.nextToken());
+
+ assertToken(JsonToken.FIELD_NAME, p.nextToken());
+ assertEquals("a", p.getText());
+ assertToken(JsonToken.VALUE_TRUE, p.nextToken());
+
+ assertToken(JsonToken.FIELD_NAME, p.nextToken());
+ assertEquals("b", p.getText());
+ assertToken(JsonToken.VALUE_FALSE, p.nextToken());
+
+ assertEquals(JsonToken.END_OBJECT, p.nextToken());
+ assertEnd(p);
+ }
+
+ @Test
+ public void testObjectInnerComma() throws Exception {
+ String json = "{\"a\": true,, \"b\": false}";
+
+ JsonParser p = createParser(factory, mode, json);
+
+ assertEquals(JsonToken.START_OBJECT, p.nextToken());
+
+ assertToken(JsonToken.FIELD_NAME, p.nextToken());
+ assertEquals("a", p.getText());
+ assertToken(JsonToken.VALUE_TRUE, p.nextToken());
+
+ assertUnexpected(p, ',');
+ }
+
+ @Test
+ public void testObjectLeadingComma() throws Exception {
+ String json = "{,\"a\": true, \"b\": false}";
+
+ JsonParser p = createParser(factory, mode, json);
+
+ assertEquals(JsonToken.START_OBJECT, p.nextToken());
+
+ assertUnexpected(p, ',');
+ }
+
+ @Test
+ public void testObjectTrailingComma() throws Exception {
+ String json = "{\"a\": true, \"b\": false,}";
+
+ JsonParser p = createParser(factory, mode, json);
+
+ assertEquals(JsonToken.START_OBJECT, p.nextToken());
+
+ assertToken(JsonToken.FIELD_NAME, p.nextToken());
+ assertEquals("a", p.getText());
+ assertToken(JsonToken.VALUE_TRUE, p.nextToken());
+
+ assertToken(JsonToken.FIELD_NAME, p.nextToken());
+ assertEquals("b", p.getText());
+ assertToken(JsonToken.VALUE_FALSE, p.nextToken());
+
+ if (features.contains(Feature.ALLOW_TRAILING_COMMA)) {
+ assertToken(JsonToken.END_OBJECT, p.nextToken());
+ assertEnd(p);
+ } else {
+ assertUnexpected(p, '}');
+ }
+ }
+
+ @Test
+ public void testObjectTrailingCommas() throws Exception {
+ String json = "{\"a\": true, \"b\": false,,}";
+
+ JsonParser p = createParser(factory, mode, json);
+
+ assertEquals(JsonToken.START_OBJECT, p.nextToken());
+
+ assertToken(JsonToken.FIELD_NAME, p.nextToken());
+ assertEquals("a", p.getText());
+ assertToken(JsonToken.VALUE_TRUE, p.nextToken());
+
+ assertToken(JsonToken.FIELD_NAME, p.nextToken());
+ assertEquals("b", p.getText());
+ assertToken(JsonToken.VALUE_FALSE, p.nextToken());
+
+ assertUnexpected(p, ',');
+ }
+
+ private void assertEnd(JsonParser p) throws IOException {
+ // Issue #325
+ if (!(p instanceof UTF8DataInputJsonParser)) {
+ JsonToken next = p.nextToken();
+ assertNull("expected end of stream but found " + next, next);
+ }
+ }
+
+ private void assertUnexpected(JsonParser p, char c) throws IOException {
+ try {
+ p.nextToken();
+ fail("No exception thrown");
+ } catch (Exception e) {
+ verifyException(e, String.format("Unexpected character ('%s' (code %d))", c, (int) c));
+ }
+ }
+}