diff --git a/src/main/java/com/fasterxml/jackson/core/JsonParser.java b/src/main/java/com/fasterxml/jackson/core/JsonParser.java index 056562e5ea..7ba6272cd5 100644 --- a/src/main/java/com/fasterxml/jackson/core/JsonParser.java +++ b/src/main/java/com/fasterxml/jackson/core/JsonParser.java @@ -226,7 +226,23 @@ 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, under this regime [true,true,] is equivalent to + * [true, true] and {"a": true,} is equivalent to + * {"a": true}. + *

+ * Since the JSON specification does not permit trailing commas, this is a non-standard + * feature, and as such disabled by default. + * + * @since 2.8 + */ + ALLOW_TRAILING_COMMAS(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 2845dada22..2cd8bddb4a 100644 --- a/src/main/java/com/fasterxml/jackson/core/json/ReaderBasedJsonParser.java +++ b/src/main/java/com/fasterxml/jackson/core/json/ReaderBasedJsonParser.java @@ -609,26 +609,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_COMMAS) && (i == INT_RBRACKET || i == INT_RCURLY)) { + _closeScope(i); + return _currToken; + } } /* And should we now have a name? Always true for Object contexts, since @@ -768,26 +762,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_COMMAS) && (i == INT_RBRACKET || i == INT_RCURLY)) { + _closeScope(i); + return false; + } } if (!_parsingContext.inObject()) { @@ -2786,4 +2774,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/UTF8StreamJsonParser.java b/src/main/java/com/fasterxml/jackson/core/json/UTF8StreamJsonParser.java index 70cc77cf77..40909c3987 100644 --- a/src/main/java/com/fasterxml/jackson/core/json/UTF8StreamJsonParser.java +++ b/src/main/java/com/fasterxml/jackson/core/json/UTF8StreamJsonParser.java @@ -712,21 +712,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? @@ -735,6 +723,12 @@ public JsonToken nextToken() throws IOException _reportUnexpectedChar(i, "was expecting comma to separate "+_parsingContext.getTypeDesc()+" entries"); } i = _skipWS(); + + // Was that a trailing comma? + if (isEnabled(Feature.ALLOW_TRAILING_COMMAS) && (i == INT_RBRACKET || i == INT_RCURLY)) { + _closeScope(i); + return _currToken; + } } /* And should we now have a name? Always true for @@ -904,22 +898,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; } @@ -929,6 +909,12 @@ public boolean nextFieldName(SerializableString str) throws IOException _reportUnexpectedChar(i, "was expecting comma to separate "+_parsingContext.getTypeDesc()+" entries"); } i = _skipWS(); + + // Was that a trailing comma? + if (isEnabled(Feature.ALLOW_TRAILING_COMMAS) && (i == INT_RBRACKET || i == INT_RCURLY)) { + _closeScope(i); + return false; + } } if (!_parsingContext.inObject()) { @@ -991,22 +977,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; } @@ -1016,7 +988,14 @@ public String nextFieldName() throws IOException _reportUnexpectedChar(i, "was expecting comma to separate "+_parsingContext.getTypeDesc()+" entries"); } i = _skipWS(); + + // Was that a trailing comma? + if (isEnabled(Feature.ALLOW_TRAILING_COMMAS) && (i == INT_RBRACKET || i == INT_RCURLY)) { + _closeScope(i); + return null; + } } + if (!_parsingContext.inObject()) { _updateLocation(); _nextTokenNotInObject(i); @@ -3703,6 +3682,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/json/TestParserNonStandard.java b/src/test/java/com/fasterxml/jackson/core/json/TestParserNonStandard.java index 0538b80520..83a0ac5bf8 100644 --- a/src/test/java/com/fasterxml/jackson/core/json/TestParserNonStandard.java +++ b/src/test/java/com/fasterxml/jackson/core/json/TestParserNonStandard.java @@ -71,6 +71,14 @@ public void testAllowInfinity() throws Exception { _testAllowInf(false); _testAllowInf(true); } + + // [#118]: allow trailing commas + public void testAllowTrailingCommas() throws Exception { + _testAllowTrailingCommaInArray(false); + _testAllowTrailingCommaInArray(true); + _testAllowTrailingCommaInObject(false); + _testAllowTrailingCommaInObject(true); + } /* /**************************************************************** @@ -502,4 +510,177 @@ private void _testAllowInf(boolean useStream) throws Exception jp.close(); } + + private void _testAllowTrailingCommaInArray(boolean useStream) throws Exception { + String json = "[true,]"; + JsonFactory f = new JsonFactory(); + assertFalse(f.isEnabled(JsonParser.Feature.ALLOW_TRAILING_COMMAS)); + + // Without enabling, should get an exception + JsonParser jp = useStream ? createParserUsingStream(f, json, "UTF-8") + : createParserUsingReader(f, json); + + assertToken(JsonToken.START_ARRAY, jp.nextToken()); + assertToken(JsonToken.VALUE_TRUE, jp.nextToken()); + try { + jp.nextToken(); + fail("Expected exception"); + } catch (Exception e) { + verifyException(e, "Unexpected character (']' (code 93)): expected a value"); + } finally { + jp.close(); + } + + // Enable feature + f.configure(JsonParser.Feature.ALLOW_TRAILING_COMMAS, true); + + jp = useStream ? createParserUsingStream(f, json, "UTF-8") + : createParserUsingReader(f, json); + assertToken(JsonToken.START_ARRAY, jp.nextToken()); + assertToken(JsonToken.VALUE_TRUE, jp.nextToken()); + assertToken(JsonToken.END_ARRAY, jp.nextToken()); + jp.close(); + + // Try with a more advanced case + json = "[true, 3, \"a\", null,]"; + jp = useStream ? createParserUsingStream(f, json, "UTF-8") + : createParserUsingReader(f, json); + + assertToken(JsonToken.START_ARRAY, jp.nextToken()); + assertToken(JsonToken.VALUE_TRUE, jp.nextToken()); + assertToken(JsonToken.VALUE_NUMBER_INT, jp.nextToken()); + assertToken(JsonToken.VALUE_STRING, jp.nextToken()); + assertToken(JsonToken.VALUE_NULL, jp.nextToken()); + assertToken(JsonToken.END_ARRAY, jp.nextToken()); + jp.close(); + + // Consecutive trailing commas are prohibited + json = "[true,,]"; + jp = useStream ? createParserUsingStream(f, json, "UTF-8") + : createParserUsingReader(f, json); + assertToken(JsonToken.START_ARRAY, jp.nextToken()); + assertToken(JsonToken.VALUE_TRUE, jp.nextToken()); + try { + jp.nextToken(); + } catch (Exception e) { + verifyException(e, "Unexpected character (',' (code 44)): expected a valid value (number, String, array, object, 'true', 'false' or 'null')"); + } finally { + jp.close(); + } + + // Spurious commas are prohibited + json = "[,true]"; + jp = useStream ? createParserUsingStream(f, json, "UTF-8") + : createParserUsingReader(f, json); + assertToken(JsonToken.START_ARRAY, jp.nextToken()); + try { + jp.nextToken(); + } catch (Exception e) { + verifyException(e, "Unexpected character (',' (code 44)): expected a valid value (number, String, array, object, 'true', 'false' or 'null')"); + } finally { + jp.close(); + } + + // Empty with comma is prohibited + json = "[,]"; + jp = useStream ? createParserUsingStream(f, json, "UTF-8") + : createParserUsingReader(f, json); + assertToken(JsonToken.START_ARRAY, jp.nextToken()); + try { + jp.nextToken(); + } catch (Exception e) { + verifyException(e, "Unexpected character (',' (code 44)): expected a valid value (number, String, array, object, 'true', 'false' or 'null')"); + } finally { + jp.close(); + } + } + + private void _testAllowTrailingCommaInObject(boolean useStream) throws Exception { + String json = "{\"a\": true,}"; + JsonFactory f = new JsonFactory(); + assertFalse(f.isEnabled(JsonParser.Feature.ALLOW_TRAILING_COMMAS)); + + // Without enabling, should get an exception + JsonParser jp = useStream ? createParserUsingStream(f, json, "UTF-8") + : createParserUsingReader(f, json); + + assertToken(JsonToken.START_OBJECT, jp.nextToken()); + assertToken(JsonToken.FIELD_NAME, jp.nextToken()); + assertToken(JsonToken.VALUE_TRUE, jp.nextToken()); + try { + jp.nextToken(); + fail("Expected exception"); + } catch (Exception e) { + verifyException(e, "Unexpected character ('}' (code 125)): was expecting double-quote to start field name"); + } finally { + jp.close(); + } + + // Enable feature + f.configure(JsonParser.Feature.ALLOW_TRAILING_COMMAS, true); + + jp = useStream ? createParserUsingStream(f, json, "UTF-8") + : createParserUsingReader(f, json); + assertToken(JsonToken.START_OBJECT, jp.nextToken()); + assertToken(JsonToken.FIELD_NAME, jp.nextToken()); + assertToken(JsonToken.VALUE_TRUE, jp.nextToken()); + assertToken(JsonToken.END_OBJECT, jp.nextToken()); + jp.close(); + + // Check with a more advanced case + json = "{ \"a\": true, \"b\": 3, \"c\": null, }"; + jp = useStream ? createParserUsingStream(f, json, "UTF-8") + : createParserUsingReader(f, json); + + assertToken(JsonToken.START_OBJECT, jp.nextToken()); + assertToken(JsonToken.FIELD_NAME, jp.nextToken()); + assertToken(JsonToken.VALUE_TRUE, jp.nextToken()); + assertToken(JsonToken.FIELD_NAME, jp.nextToken()); + assertToken(JsonToken.VALUE_NUMBER_INT, jp.nextToken()); + assertToken(JsonToken.FIELD_NAME, jp.nextToken()); + assertToken(JsonToken.VALUE_NULL, jp.nextToken()); + assertToken(JsonToken.END_OBJECT, jp.nextToken()); + jp.close(); + + // Consecutive trailing commas are prohibited + json = "{\"a\": true,,}"; + jp = useStream ? createParserUsingStream(f, json, "UTF-8") + : createParserUsingReader(f, json); + assertToken(JsonToken.START_OBJECT, jp.nextToken()); + assertToken(JsonToken.FIELD_NAME, jp.nextToken()); + assertToken(JsonToken.VALUE_TRUE, jp.nextToken()); + try { + jp.nextToken(); + } catch (Exception e) { + verifyException(e, "Unexpected character (',' (code 44)): was expecting double-quote to start field name"); + } finally { + jp.close(); + } + + // Spurious commas are prohibited + json = "{,\"a\": true}"; + jp = useStream ? createParserUsingStream(f, json, "UTF-8") + : createParserUsingReader(f, json); + assertToken(JsonToken.START_OBJECT, jp.nextToken()); + try { + jp.nextToken(); + } catch (Exception e) { + verifyException(e, "Unexpected character (',' (code 44)): was expecting double-quote to start field name"); + } finally { + jp.close(); + } + + // Empty with comma is prohibited + json = "{,}"; + jp = useStream ? createParserUsingStream(f, json, "UTF-8") + : createParserUsingReader(f, json); + assertToken(JsonToken.START_OBJECT, jp.nextToken()); + try { + jp.nextToken(); + } catch (Exception e) { + verifyException(e, "Unexpected character (',' (code 44)): was expecting double-quote to start field name"); + } finally { + jp.close(); + } + } }