diff --git a/src/main/java/com/fasterxml/jackson/core/JsonParser.java b/src/main/java/com/fasterxml/jackson/core/JsonParser.java index 2fd00111c9..661cad852c 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 @@ public enum Feature { * * @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. + *
+ * For example, when this feature is enabled, [true,true,]
is equivalent to
+ * [true, true]
and {"a": true,}
is equivalent to
+ * {"a": true}
.
+ *
+ * When combined with ALLOW_MISSING_VALUES
, this feature takes priority, and
+ * the final trailing comma in an array declaration does not imply a missing
+ * (null
) value. For example, when both ALLOW_MISSING_VALUES
+ * and ALLOW_TRAILING_COMMA
are enabled, [true,true,]
is
+ * equivalent to [true, true]
, and [true,true,,]
is equivalent to
+ * [true, true, null]
.
+ *
+ * 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 dc80f62e6f..b27de2e774 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 @@ public final JsonToken nextToken() throws IOException
_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 @@ public boolean nextFieldName(SerializableString sstr) throws IOException
}
_binaryValue = null;
- 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;
+ // Closing scope?
+ if (i == INT_RBRACKET || i == INT_RCURLY) {
+ _closeScope(i);
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 @@ protected void _reportInvalidToken(String matchedPart, String msg) throws IOExce
}
_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 8bc3789ac6..de5d081252 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 @@ public JsonToken nextToken() throws IOException
_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 @@ public JsonToken nextToken() throws IOException
_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 @@ public JsonLocation getCurrentLocation() {
/**********************************************************
*/
+ 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 5a0dcdafb5..23f52bf4a4 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 @@ public JsonToken nextToken() throws IOException
_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 @@ public JsonToken nextToken() throws IOException
_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 @@ public boolean nextFieldName(SerializableString str) throws IOException
_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 @@ public boolean nextFieldName(SerializableString str) throws IOException
_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 @@ public String nextFieldName() throws IOException
}
_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 @@ public String nextFieldName() throws IOException
_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 final void _updateNameLocation()
/**********************************************************
*/
+ 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 0000000000..972b6505f2
--- /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