From 1f308db7c48048e15c9953ebd6499c84dc135f3b Mon Sep 17 00:00:00 2001 From: Sean Leary Date: Sat, 14 Dec 2024 10:01:27 -0600 Subject: [PATCH 1/6] restore-jsonparserconfiguration: Restore methods to be used for strict mode --- src/main/java/org/json/JSONArray.java | 61 +++++++++++++------ .../org/json/JSONParserConfiguration.java | 36 +++++++++++ 2 files changed, 80 insertions(+), 17 deletions(-) diff --git a/src/main/java/org/json/JSONArray.java b/src/main/java/org/json/JSONArray.java index 87bd99b..fe56b50 100644 --- a/src/main/java/org/json/JSONArray.java +++ b/src/main/java/org/json/JSONArray.java @@ -83,11 +83,22 @@ public class JSONArray implements Iterable { * If there is a syntax error. */ public JSONArray(JSONTokener x) throws JSONException { + this(x, new JSONParserConfiguration()); + } + + /** + * Constructs a JSONArray from a JSONTokener and a JSONParserConfiguration. + * + * @param x A JSONTokener instance from which the JSONArray is constructed. + * @param jsonParserConfiguration A JSONParserConfiguration instance that controls the behavior of the parser. + * @throws JSONException If a syntax error occurs during the construction of the JSONArray. + */ + public JSONArray(JSONTokener x, JSONParserConfiguration jsonParserConfiguration) throws JSONException { this(); if (x.nextClean() != '[') { throw x.syntaxError("A JSONArray text must start with '['"); } - + char nextChar = x.nextClean(); if (nextChar == 0) { // array is unclosed. No ']' found, instead EOF @@ -104,27 +115,28 @@ public class JSONArray implements Iterable { this.myArrayList.add(x.nextValue()); } switch (x.nextClean()) { - case 0: - // array is unclosed. No ']' found, instead EOF - throw x.syntaxError("Expected a ',' or ']'"); - case ',': - nextChar = x.nextClean(); - if (nextChar == 0) { + case 0: // array is unclosed. No ']' found, instead EOF throw x.syntaxError("Expected a ',' or ']'"); - } - if (nextChar == ']') { + case ',': + nextChar = x.nextClean(); + if (nextChar == 0) { + // array is unclosed. No ']' found, instead EOF + throw x.syntaxError("Expected a ',' or ']'"); + } + if (nextChar == ']') { + return; + } + x.back(); + break; + case ']': return; - } - x.back(); - break; - case ']': - return; - default: - throw x.syntaxError("Expected a ',' or ']'"); + default: + throw x.syntaxError("Expected a ',' or ']'"); } } } + } /** @@ -138,7 +150,22 @@ public class JSONArray implements Iterable { * If there is a syntax error. */ public JSONArray(String source) throws JSONException { - this(new JSONTokener(source)); + this(source, new JSONParserConfiguration()); + } + + /** + * Construct a JSONArray from a source JSON text. + * + * @param source + * A string that begins with [ (left + * bracket) and ends with ] + *  (right bracket). + * @param jsonParserConfiguration the parser config object + * @throws JSONException + * If there is a syntax error. + */ + public JSONArray(String source, JSONParserConfiguration jsonParserConfiguration) throws JSONException { + this(new JSONTokener(source), jsonParserConfiguration); } /** diff --git a/src/main/java/org/json/JSONParserConfiguration.java b/src/main/java/org/json/JSONParserConfiguration.java index 190daeb..46996cb 100644 --- a/src/main/java/org/json/JSONParserConfiguration.java +++ b/src/main/java/org/json/JSONParserConfiguration.java @@ -17,6 +17,12 @@ public class JSONParserConfiguration extends ParserConfiguration { this.overwriteDuplicateKey = false; } + /** + * This flag, when set to true, instructs the parser to enforce strict mode when parsing JSON text. + * Garbage chars at the end of the doc, unquoted string, and single-quoted strings are all disallowed. + */ + private boolean strictMode; + @Override protected JSONParserConfiguration clone() { JSONParserConfiguration clone = new JSONParserConfiguration(); @@ -58,6 +64,23 @@ public class JSONParserConfiguration extends ParserConfiguration { return clone; } + /** + * Sets the strict mode configuration for the JSON parser. + *

+ * When strict mode is enabled, the parser will throw a JSONException if it encounters an invalid character + * immediately following the final ']' character in the input. This is useful for ensuring strict adherence to the + * JSON syntax, as any characters after the final closing bracket of a JSON array are considered invalid. + * + * @param mode a boolean value indicating whether strict mode should be enabled or not + * @return a new JSONParserConfiguration instance with the updated strict mode setting + */ + public JSONParserConfiguration withStrictMode(final boolean mode) { + JSONParserConfiguration clone = this.clone(); + clone.strictMode = mode; + + return clone; + } + /** * The parser's behavior when meeting duplicate keys, controls whether the parser should * overwrite duplicate keys or not. @@ -67,4 +90,17 @@ public class JSONParserConfiguration extends ParserConfiguration { public boolean isOverwriteDuplicateKey() { return this.overwriteDuplicateKey; } + + /** + * Retrieves the current strict mode setting of the JSON parser. + *

+ * Strict mode, when enabled, instructs the parser to throw a JSONException if it encounters an invalid character + * immediately following the final ']' character in the input. This ensures strict adherence to the JSON syntax, as + * any characters after the final closing bracket of a JSON array are considered invalid. + * + * @return the current strict mode setting. True if strict mode is enabled, false otherwise. + */ + public boolean isStrictMode() { + return this.strictMode; + } } From 80b2672f77d78598186982394479ac68497c0f56 Mon Sep 17 00:00:00 2001 From: Sean Leary Date: Sat, 14 Dec 2024 10:07:26 -0600 Subject: [PATCH 2/6] restore-jsonparserconfiguration: clean up some whitespace --- src/main/java/org/json/JSONArray.java | 30 +++++++++++++-------------- 1 file changed, 15 insertions(+), 15 deletions(-) diff --git a/src/main/java/org/json/JSONArray.java b/src/main/java/org/json/JSONArray.java index fe56b50..5b3cb2b 100644 --- a/src/main/java/org/json/JSONArray.java +++ b/src/main/java/org/json/JSONArray.java @@ -115,24 +115,24 @@ public class JSONArray implements Iterable { this.myArrayList.add(x.nextValue()); } switch (x.nextClean()) { - case 0: + case 0: + // array is unclosed. No ']' found, instead EOF + throw x.syntaxError("Expected a ',' or ']'"); + case ',': + nextChar = x.nextClean(); + if (nextChar == 0) { // array is unclosed. No ']' found, instead EOF throw x.syntaxError("Expected a ',' or ']'"); - case ',': - nextChar = x.nextClean(); - if (nextChar == 0) { - // array is unclosed. No ']' found, instead EOF - throw x.syntaxError("Expected a ',' or ']'"); - } - if (nextChar == ']') { - return; - } - x.back(); - break; - case ']': + } + if (nextChar == ']') { return; - default: - throw x.syntaxError("Expected a ',' or ']'"); + } + x.back(); + break; + case ']': + return; + default: + throw x.syntaxError("Expected a ',' or ']'"); } } } From 1f0729cadbae625344ed49cf71425d15a62e3aad Mon Sep 17 00:00:00 2001 From: Sean Leary Date: Sat, 14 Dec 2024 14:40:40 -0600 Subject: [PATCH 3/6] restore-jsonparserconfiguration: strict mode initial attempt. Still missing all JSONObject test cases and strict mode sanity check. Might be able to simplify implementation a bit more --- src/main/java/org/json/JSONArray.java | 31 +- src/main/java/org/json/JSONObject.java | 17 + src/main/java/org/json/JSONTokener.java | 33 +- .../junit/JSONParserConfigurationTest.java | 287 +++++++++++++++- src/test/resources/compliantJsonArray.json | 317 ++++++++++++++++++ 5 files changed, 679 insertions(+), 6 deletions(-) create mode 100644 src/test/resources/compliantJsonArray.json diff --git a/src/main/java/org/json/JSONArray.java b/src/main/java/org/json/JSONArray.java index 5b3cb2b..759acd7 100644 --- a/src/main/java/org/json/JSONArray.java +++ b/src/main/java/org/json/JSONArray.java @@ -67,6 +67,10 @@ public class JSONArray implements Iterable { */ private final ArrayList myArrayList; + private JSONTokener jsonTokener; + + private JSONParserConfiguration jsonParserConfiguration; + /** * Construct an empty JSONArray. */ @@ -95,6 +99,15 @@ public class JSONArray implements Iterable { */ public JSONArray(JSONTokener x, JSONParserConfiguration jsonParserConfiguration) throws JSONException { this(); + + if (this.jsonParserConfiguration == null) { + this.jsonParserConfiguration = jsonParserConfiguration; + } + if (this.jsonTokener == null) { + this.jsonTokener = x; + this.jsonTokener.setJsonParserConfiguration(this.jsonParserConfiguration); + } + if (x.nextClean() != '[') { throw x.syntaxError("A JSONArray text must start with '['"); } @@ -125,6 +138,9 @@ public class JSONArray implements Iterable { throw x.syntaxError("Expected a ',' or ']'"); } if (nextChar == ']') { + if (jsonParserConfiguration.isStrictMode()) { + throw x.syntaxError("Expected another array element"); + } return; } x.back(); @@ -136,7 +152,6 @@ public class JSONArray implements Iterable { } } } - } /** @@ -151,6 +166,13 @@ public class JSONArray implements Iterable { */ public JSONArray(String source) throws JSONException { this(source, new JSONParserConfiguration()); + if (this.jsonParserConfiguration.isStrictMode()) { + char c = jsonTokener.nextClean(); + if (c != 0) { + throw jsonTokener.syntaxError(String.format("invalid character '%s' found after end of array", c)); + + } + } } /** @@ -166,6 +188,13 @@ public class JSONArray implements Iterable { */ public JSONArray(String source, JSONParserConfiguration jsonParserConfiguration) throws JSONException { this(new JSONTokener(source), jsonParserConfiguration); + if (this.jsonParserConfiguration.isStrictMode()) { + char c = jsonTokener.nextClean(); + if (c != 0) { + throw jsonTokener.syntaxError(String.format("invalid character '%s' found after end of array", c)); + + } + } } /** diff --git a/src/main/java/org/json/JSONObject.java b/src/main/java/org/json/JSONObject.java index 26a68c6..47d263a 100644 --- a/src/main/java/org/json/JSONObject.java +++ b/src/main/java/org/json/JSONObject.java @@ -152,6 +152,10 @@ public class JSONObject { */ public static final Object NULL = new Null(); + private JSONTokener jsonTokener; + + private JSONParserConfiguration jsonParserConfiguration; + /** * Construct an empty JSONObject. */ @@ -211,6 +215,15 @@ public class JSONObject { */ public JSONObject(JSONTokener x, JSONParserConfiguration jsonParserConfiguration) throws JSONException { this(); + + if (this.jsonParserConfiguration == null) { + this.jsonParserConfiguration = jsonParserConfiguration; + } + if (this.jsonTokener == null) { + this.jsonTokener = x; + this.jsonTokener.setJsonParserConfiguration(this.jsonParserConfiguration); + } + char c; String key; @@ -433,6 +446,10 @@ public class JSONObject { */ public JSONObject(String source) throws JSONException { this(source, new JSONParserConfiguration()); + if (this.jsonParserConfiguration.isStrictMode() && + this.jsonTokener.nextClean() != 0) { + throw new JSONException("Unparsed characters found at end of input text"); + } } /** diff --git a/src/main/java/org/json/JSONTokener.java b/src/main/java/org/json/JSONTokener.java index b8808bb..2fcc24a 100644 --- a/src/main/java/org/json/JSONTokener.java +++ b/src/main/java/org/json/JSONTokener.java @@ -32,6 +32,7 @@ public class JSONTokener { /** the number of characters read in the previous line. */ private long characterPreviousLine; + private JSONParserConfiguration jsonParserConfiguration; /** * Construct a JSONTokener from a Reader. The caller must close the Reader. @@ -70,6 +71,21 @@ public class JSONTokener { this(new StringReader(s)); } + /** + * Getter + * @return jsonParserConfiguration + */ + public JSONParserConfiguration getJsonParserConfiguration() { + return jsonParserConfiguration; + } + + /** + * Setter + * @param jsonParserConfiguration new value for jsonParserConfiguration + */ + public void setJsonParserConfiguration(JSONParserConfiguration jsonParserConfiguration) { + this.jsonParserConfiguration = jsonParserConfiguration; + } /** * Back up one character. This provides a sort of lookahead capability, @@ -409,14 +425,14 @@ public class JSONTokener { case '{': this.back(); try { - return new JSONObject(this); + return new JSONObject(this, jsonParserConfiguration); } catch (StackOverflowError e) { throw new JSONException("JSON Array or Object depth too large to process.", e); } case '[': this.back(); try { - return new JSONArray(this); + return new JSONArray(this, jsonParserConfiguration); } catch (StackOverflowError e) { throw new JSONException("JSON Array or Object depth too large to process.", e); } @@ -427,6 +443,11 @@ public class JSONTokener { Object nextSimpleValue(char c) { String string; + if (jsonParserConfiguration != null && + jsonParserConfiguration.isStrictMode() && + c == '\'') { + throw this.syntaxError("Single quote wrap not allowed in strict mode"); + } switch (c) { case '"': case '\'': @@ -455,7 +476,13 @@ public class JSONTokener { if ("".equals(string)) { throw this.syntaxError("Missing value"); } - return JSONObject.stringToValue(string); + Object obj = JSONObject.stringToValue(string); + if (jsonParserConfiguration != null && + jsonParserConfiguration.isStrictMode() && + obj instanceof String) { + throw this.syntaxError(String.format("Value '%s' is not surrounded by quotes", obj)); + } + return obj; } diff --git a/src/test/java/org/json/junit/JSONParserConfigurationTest.java b/src/test/java/org/json/junit/JSONParserConfigurationTest.java index 509b988..dbe436a 100644 --- a/src/test/java/org/json/junit/JSONParserConfigurationTest.java +++ b/src/test/java/org/json/junit/JSONParserConfigurationTest.java @@ -1,12 +1,21 @@ package org.json.junit; +import org.json.JSONArray; import org.json.JSONException; import org.json.JSONObject; import org.json.JSONParserConfiguration; +import org.junit.Ignore; import org.junit.Test; -import static org.junit.Assert.assertEquals; -import static org.junit.Assert.assertTrue; +import java.io.IOException; +import java.nio.file.Files; +import java.nio.file.Paths; +import java.util.Arrays; +import java.util.List; +import java.util.stream.Collectors; +import java.util.stream.Stream; + +import static org.junit.Assert.*; public class JSONParserConfigurationTest { private static final String TEST_SOURCE = "{\"key\": \"value1\", \"key\": \"value2\"}"; @@ -43,4 +52,278 @@ public class JSONParserConfigurationTest { assertTrue(jsonParserConfiguration.isOverwriteDuplicateKey()); assertEquals(42, jsonParserConfiguration.getMaxNestingDepth()); } + + @Test + public void givenInvalidInputArrays_testStrictModeTrue_shouldThrowJsonException() { + JSONParserConfiguration jsonParserConfiguration = new JSONParserConfiguration() + .withStrictMode(true); + + List strictModeInputTestCases = getNonCompliantJSONList(); + + // this is a lot easier to debug when things stop working + for (int i = 0; i < strictModeInputTestCases.size(); ++i) { + String testCase = strictModeInputTestCases.get(i); + try { + JSONArray jsonArray = new JSONArray(testCase, jsonParserConfiguration); + String s = jsonArray.toString(); + System.out.println("Expected an exception, but got: " + s + " Noncompliant Array index: " + i); + } catch (Exception e) { + // its all good + } + } + } + + @Test + public void givenEmptyArray_testStrictModeTrue_shouldNotThrowJsonException() { + JSONParserConfiguration jsonParserConfiguration = new JSONParserConfiguration() + .withStrictMode(true); + + String testCase = "[]"; + + JSONArray jsonArray = new JSONArray(testCase, jsonParserConfiguration); + + assertEquals(testCase, jsonArray.toString()); + } + + @Test + public void givenValidDoubleArray_testStrictModeTrue_shouldNotThrowJsonException() { + JSONParserConfiguration jsonParserConfiguration = new JSONParserConfiguration() + .withStrictMode(true); + + String testCase = "[[\"c\"], [10.2], [true, false, true]]"; + + JSONArray jsonArray = new JSONArray(testCase, jsonParserConfiguration); + JSONArray arrayShouldContainStringAt0 = jsonArray.getJSONArray(0); + JSONArray arrayShouldContainNumberAt0 = jsonArray.getJSONArray(1); + JSONArray arrayShouldContainBooleanAt0 = jsonArray.getJSONArray(2); + + assertTrue(arrayShouldContainStringAt0.get(0) instanceof String); + assertTrue(arrayShouldContainNumberAt0.get(0) instanceof Number); + assertTrue(arrayShouldContainBooleanAt0.get(0) instanceof Boolean); + } + + @Test + public void givenValidEmptyArrayInsideArray_testStrictModeTrue_shouldNotThrowJsonException(){ + JSONParserConfiguration jsonParserConfiguration = new JSONParserConfiguration() + .withStrictMode(true); + + String testCase = "[[]]"; + + JSONArray jsonArray = new JSONArray(testCase, jsonParserConfiguration); + + assertEquals(testCase, jsonArray.toString()); + } + + @Test + public void givenValidEmptyArrayInsideArray_testStrictModeFalse_shouldNotThrowJsonException(){ + JSONParserConfiguration jsonParserConfiguration = new JSONParserConfiguration() + .withStrictMode(false); + + String testCase = "[[]]"; + + JSONArray jsonArray = new JSONArray(testCase, jsonParserConfiguration); + + assertEquals(testCase, jsonArray.toString()); + } + + @Test + public void givenInvalidString_testStrictModeTrue_shouldThrowJsonException() { + JSONParserConfiguration jsonParserConfiguration = new JSONParserConfiguration() + .withStrictMode(true); + + String testCase = "[badString]"; + + JSONException je = assertThrows(JSONException.class, () -> new JSONArray(testCase, jsonParserConfiguration)); + + assertEquals("Value 'badString' is not surrounded by quotes at 10 [character 11 line 1]", je.getMessage()); + } + + @Test + public void allowNullInStrictMode() { + String expected = "[null]"; + JSONArray jsonArray = new JSONArray(expected, new JSONParserConfiguration().withStrictMode(true)); + assertEquals(expected, jsonArray.toString()); + } + + @Test + public void shouldHandleNumericArray() { + String expected = "[10]"; + JSONArray jsonArray = new JSONArray(expected, new JSONParserConfiguration().withStrictMode(true)); + assertEquals(expected, jsonArray.toString()); + } + + @Test + public void givenCompliantJSONArrayFile_testStrictModeTrue_shouldNotThrowAnyException() throws IOException { + try (Stream lines = Files.lines(Paths.get("src/test/resources/compliantJsonArray.json"))) { + String compliantJsonArrayAsString = lines.collect(Collectors.joining()); + JSONParserConfiguration jsonParserConfiguration = new JSONParserConfiguration() + .withStrictMode(true); + + new JSONArray(compliantJsonArrayAsString, jsonParserConfiguration); + } + + } + + @Test + public void givenInvalidInputArrays_testStrictModeFalse_shouldNotThrowAnyException() { + JSONParserConfiguration jsonParserConfiguration = new JSONParserConfiguration() + .withStrictMode(false); + + List strictModeInputTestCases = getNonCompliantJSONList(); + + // this is a lot easier to debug when things stop working + for (int i = 0; i < strictModeInputTestCases.size(); ++i) { + String testCase = strictModeInputTestCases.get(i); + try { + JSONArray jsonArray = new JSONArray(testCase, jsonParserConfiguration); + } catch (Exception e) { + System.out.println("Unexpected exception: " + e.getMessage() + " Noncompliant Array index: " + i); + } + } + strictModeInputTestCases.forEach(testCase -> new JSONArray(testCase, jsonParserConfiguration)); + } + + @Test + public void givenInvalidInputArray_testStrictModeTrue_shouldThrowInvalidCharacterErrorMessage() { + JSONParserConfiguration jsonParserConfiguration = new JSONParserConfiguration() + .withStrictMode(true); + + String testCase = "[1,2];[3,4]"; + + JSONException je = assertThrows("expected non-compliant array but got instead: " + testCase, + JSONException.class, () -> new JSONArray(testCase, jsonParserConfiguration)); + + assertEquals("invalid character ';' found after end of array at 6 [character 7 line 1]", je.getMessage()); + } + + @Test + public void givenInvalidInputArrayWithNumericStrings_testStrictModeTrue_shouldThrowInvalidCharacterErrorMessage() { + JSONParserConfiguration jsonParserConfiguration = new JSONParserConfiguration() + .withStrictMode(true); + + String testCase = "[\"1\",\"2\"];[3,4]"; + + JSONException je = assertThrows("expected non-compliant array but got instead: " + testCase, + JSONException.class, () -> new JSONArray(testCase, jsonParserConfiguration)); + + assertEquals("invalid character ';' found after end of array at 10 [character 11 line 1]", je.getMessage()); + } + + @Test + public void givenInvalidInputArray_testStrictModeTrue_shouldThrowValueNotSurroundedByQuotesErrorMessage() { + JSONParserConfiguration jsonParserConfiguration = new JSONParserConfiguration() + .withStrictMode(true); + + String testCase = "[{\"test\": implied}]"; + + JSONException je = assertThrows("expected non-compliant array but got instead: " + testCase, + JSONException.class, () -> new JSONArray(testCase, jsonParserConfiguration)); + + assertEquals("Value 'implied' is not surrounded by quotes at 17 [character 18 line 1]", je.getMessage()); + } + + @Test + public void givenInvalidInputArray_testStrictModeFalse_shouldNotThrowAnyException() { + JSONParserConfiguration jsonParserConfiguration = new JSONParserConfiguration() + .withStrictMode(false); + + String testCase = "[{\"test\": implied}]"; + + new JSONArray(testCase, jsonParserConfiguration); + } + + @Test + public void givenNonCompliantQuotes_testStrictModeTrue_shouldThrowJsonExceptionWithConcreteErrorDescription() { + JSONParserConfiguration jsonParserConfiguration = new JSONParserConfiguration() + .withStrictMode(true); + + String testCaseOne = "[\"abc', \"test\"]"; + String testCaseTwo = "['abc\", \"test\"]"; + String testCaseThree = "['abc']"; + String testCaseFour = "[{'testField': \"testValue\"}]"; + + JSONException jeOne = assertThrows(JSONException.class, + () -> new JSONArray(testCaseOne, jsonParserConfiguration)); + JSONException jeTwo = assertThrows(JSONException.class, + () -> new JSONArray(testCaseTwo, jsonParserConfiguration)); + JSONException jeThree = assertThrows(JSONException.class, + () -> new JSONArray(testCaseThree, jsonParserConfiguration)); + JSONException jeFour = assertThrows(JSONException.class, + () -> new JSONArray(testCaseFour, jsonParserConfiguration)); + + assertEquals( + "Expected a ',' or ']' at 10 [character 11 line 1]", + jeOne.getMessage()); + assertEquals( + "Single quote wrap not allowed in strict mode at 2 [character 3 line 1]", + jeTwo.getMessage()); + assertEquals( + "Single quote wrap not allowed in strict mode at 2 [character 3 line 1]", + jeThree.getMessage()); + assertEquals( + "Single quote wrap not allowed in strict mode at 3 [character 4 line 1]", + jeFour.getMessage()); + } + + @Test + public void givenUnbalancedQuotes_testStrictModeFalse_shouldThrowJsonException() { + JSONParserConfiguration jsonParserConfiguration = new JSONParserConfiguration() + .withStrictMode(false); + + String testCaseOne = "[\"abc', \"test\"]"; + String testCaseTwo = "['abc\", \"test\"]"; + + JSONException jeOne = assertThrows(JSONException.class, + () -> new JSONArray(testCaseOne, jsonParserConfiguration)); + JSONException jeTwo = assertThrows(JSONException.class, + () -> new JSONArray(testCaseTwo, jsonParserConfiguration)); + + assertEquals("Expected a ',' or ']' at 10 [character 11 line 1]", jeOne.getMessage()); + assertEquals("Unterminated string. Character with int code 0 is not allowed within a quoted string. at 15 [character 16 line 1]", jeTwo.getMessage()); + } + + + @Test + public void givenInvalidInputArray_testStrictModeTrue_shouldThrowKeyNotSurroundedByQuotesErrorMessage() { + JSONParserConfiguration jsonParserConfiguration = new JSONParserConfiguration() + .withStrictMode(true); + + String testCase = "[{test: implied}]"; + JSONException je = assertThrows("expected non-compliant array but got instead: " + testCase, + JSONException.class, () -> new JSONArray(testCase, jsonParserConfiguration)); + + assertEquals("Value 'test' is not surrounded by quotes at 6 [character 7 line 1]", je.getMessage()); + } + + /** + * This method contains short but focused use-case samples and is exclusively used to test strictMode unit tests in + * this class. + * + * @return List with JSON strings. + */ + private List getNonCompliantJSONList() { + return Arrays.asList( + "[1],", + "[1,]", + "[[1],\"sa\",[2]]a", + "[1],\"dsa\": \"test\"", + "[[a]]", + "[]asdf", + "[]]", + "[]}", + "[][", + "[]{", + "[],", + "[]:", + "[],[", + "[],{", + "[1,2];[3,4]", + "[test]", + "[{'testSingleQuote': 'testSingleQuote'}]", + "[1, 2,3]:[4,5]", + "[{test: implied}]", + "[{\"test\": implied}]", + "[{\"number\":\"7990154836330\",\"color\":'c'},{\"number\":8784148854580,\"color\":RosyBrown},{\"number\":\"5875770107113\",\"color\":\"DarkSeaGreen\"}]", + "[{test: \"implied\"}]"); + } } diff --git a/src/test/resources/compliantJsonArray.json b/src/test/resources/compliantJsonArray.json new file mode 100644 index 0000000..d68c995 --- /dev/null +++ b/src/test/resources/compliantJsonArray.json @@ -0,0 +1,317 @@ +[ + { + "_id": "6606c27d2ab4a0102d49420a", + "index": 0, + "guid": "441331fb-84d1-4873-a649-3814621a0370", + "isActive": true, + "balance": "$2,691.63", + "picture": "http://example.abc/32x32", + "age": 26, + "eyeColor": "blue", + "name": "abc", + "gender": "female", + "company": "example", + "email": "abc@def.com", + "phone": "+1 (123) 456-7890", + "address": "123 Main St", + "about": "Laborum magna tempor officia irure cillum nulla incididunt Lorem dolor veniam elit cupidatat amet. Veniam veniam exercitation nulla consectetur officia esse ex sunt nulla nisi ea cillum nisi reprehenderit. Qui aliquip reprehenderit aliqua aliquip aliquip anim sit magna nostrud dolore veniam velit elit aliquip.\r\n", + "registered": "2016-07-22T03:18:11 -01:00", + "latitude": -21.544934, + "longitude": 72.765495, + "tags": [ + "consectetur", + "minim", + "sunt", + "in", + "ut", + "velit", + "anim" + ], + "friends": [ + { + "id": 0, + "name": "abc def" + }, + { + "id": 1, + "name": "ghi jkl" + }, + { + "id": 2, + "name": "mno pqr" + } + ], + "greeting": "Hello, abc! You have 10 unread messages.", + "favoriteFruit": "banana" + }, + { + "_id": "6606c27d0a45df5121fb765f", + "index": 1, + "guid": "fd774715-de85-44b9-b498-c214d8f68d9f", + "isActive": true, + "balance": "$2,713.96", + "picture": "http://placehold.it/32x32", + "age": 27, + "eyeColor": "green", + "name": "def", + "gender": "female", + "company": "sample", + "email": "def@abc.com", + "phone": "+1 (123) 456-78910", + "address": "1234 Main St", + "about": "Ea id cupidatat eiusmod culpa. Nulla consequat esse elit enim et pariatur eiusmod ipsum. Consequat eu non reprehenderit in.\r\n", + "registered": "2015-04-06T07:54:22 -01:00", + "latitude": 83.512347, + "longitude": -9.368739, + "tags": [ + "excepteur", + "non", + "nostrud", + "laboris", + "laboris", + "qui", + "aute" + ], + "friends": [ + { + "id": 0, + "name": "sample example" + }, + { + "id": 1, + "name": "test name" + }, + { + "id": 2, + "name": "aaa aaaa" + } + ], + "greeting": "Hello, test! You have 7 unread messages.", + "favoriteFruit": "apple" + }, + { + "_id": "6606c27dfb3a0e4e7e7183d3", + "index": 2, + "guid": "688b0c36-98e0-4ee7-86b8-863638d79b5f", + "isActive": false, + "balance": "$3,514.35", + "picture": "http://placehold.it/32x32", + "age": 32, + "eyeColor": "green", + "name": "test", + "gender": "female", + "company": "test", + "email": "test@test.com", + "phone": "+1 (123) 456-7890", + "address": "123 Main St", + "about": "Mollit officia adipisicing ex nisi non Lorem sunt quis est. Irure exercitation duis ipsum qui ullamco eu ea commodo occaecat minim proident. Incididunt nostrud ex cupidatat eiusmod mollit anim irure culpa. Labore voluptate voluptate labore nisi sit eu. Dolor sit proident velit dolor deserunt labore sit ipsum incididunt eiusmod reprehenderit voluptate. Duis anim velit officia laboris consequat officia dolor sint dolor nisi ex.\r\n", + "registered": "2021-11-02T12:50:05 -00:00", + "latitude": -82.969939, + "longitude": 86.415645, + "tags": [ + "aliquip", + "et", + "est", + "nulla", + "nulla", + "tempor", + "adipisicing" + ], + "friends": [ + { + "id": 0, + "name": "test" + }, + { + "id": 1, + "name": "sample" + }, + { + "id": 2, + "name": "example" + } + ], + "greeting": "Hello, test! You have 1 unread messages.", + "favoriteFruit": "strawberry" + }, + { + "_id": "6606c27d204bc2327fc9ba23", + "index": 3, + "guid": "be970cba-306e-4cbd-be08-c265a43a61fa", + "isActive": true, + "balance": "$3,691.63", + "picture": "http://placehold.it/32x32", + "age": 35, + "eyeColor": "brown", + "name": "another test", + "gender": "male", + "company": "TEST", + "email": "anothertest@anothertest.com", + "phone": "+1 (321) 987-6543", + "address": "123 Example Main St", + "about": "Do proident consectetur minim quis. In adipisicing culpa Lorem fugiat cillum exercitation velit velit. Non voluptate laboris deserunt veniam et sint consectetur irure aliqua quis eiusmod consectetur elit id. Ex sint do anim Lorem excepteur eu nulla.\r\n", + "registered": "2020-06-25T04:55:25 -01:00", + "latitude": 63.614955, + "longitude": -109.299405, + "tags": [ + "irure", + "esse", + "non", + "mollit", + "laborum", + "adipisicing", + "ad" + ], + "friends": [ + { + "id": 0, + "name": "test" + }, + { + "id": 1, + "name": "sample" + }, + { + "id": 2, + "name": "example" + } + ], + "greeting": "Hello, another test! You have 5 unread messages.", + "favoriteFruit": "apple" + }, + { + "_id": "6606c27df63eb5f390cb9989", + "index": 4, + "guid": "2c3e5115-758d-468e-99c5-c9afa26e1f9f", + "isActive": true, + "balance": "$1,047.20", + "picture": "http://test.it/32x32", + "age": 30, + "eyeColor": "green", + "name": "Test Name", + "gender": "female", + "company": "test", + "email": "testname@testname.com", + "phone": "+1 (999) 999-9999", + "address": "999 Test Main St", + "about": "Voluptate exercitation tempor consectetur velit magna ea occaecat cupidatat consectetur anim aute. Aliquip est aute ipsum laboris non irure qui consectetur tempor quis do ea Lorem. Cupidatat exercitation ad culpa aliqua amet commodo mollit reprehenderit exercitation adipisicing amet et laborum pariatur.\r\n", + "registered": "2023-01-19T02:43:18 -00:00", + "latitude": 14.15208, + "longitude": 170.411535, + "tags": [ + "dolor", + "qui", + "cupidatat", + "aliqua", + "laboris", + "reprehenderit", + "sint" + ], + "friends": [ + { + "id": 0, + "name": "test" + }, + { + "id": 1, + "name": "sample" + }, + { + "id": 2, + "name": "example" + } + ], + "greeting": "Hello, test! You have 6 unread messages.", + "favoriteFruit": "apple" + }, + { + "_id": "6606c27d01d19fa29853d59c", + "index": 5, + "guid": "816cda74-5d4b-498f-9724-20f340d5f5bf", + "isActive": false, + "balance": "$2,628.74", + "picture": "http://testing.it/32x32", + "age": 28, + "eyeColor": "green", + "name": "Testing", + "gender": "female", + "company": "test", + "email": "testing@testing.com", + "phone": "+1 (888) 888-8888", + "address": "123 Main St", + "about": "Cupidatat non ut nulla qui excepteur in minim non et nulla fugiat. Dolor quis laborum occaecat veniam dolor ullamco deserunt amet veniam dolor quis proident tempor laboris. In cillum duis ut quis. Aliqua cupidatat magna proident velit tempor veniam et consequat laborum ex dolore qui. Incididunt deserunt magna minim Lorem consectetur.\r\n", + "registered": "2017-10-14T11:14:08 -01:00", + "latitude": -5.345728, + "longitude": -9.706491, + "tags": [ + "officia", + "velit", + "laboris", + "qui", + "cupidatat", + "cupidatat", + "ad" + ], + "friends": [ + { + "id": 0, + "name": "test" + }, + { + "id": 1, + "name": "sample" + }, + { + "id": 2, + "name": "example" + } + ], + "greeting": "Hello, testing! You have 2 unread messages.", + "favoriteFruit": "strawberry" + }, + { + "_id": "6606c27d803003cede1d6deb", + "index": 6, + "guid": "4ee550bc-0920-4104-b3ce-ebf9db6a803f", + "isActive": true, + "balance": "$1,709.31", + "picture": "http://sample.it/32x32", + "age": 31, + "eyeColor": "blue", + "name": "Sample Name", + "gender": "female", + "company": "Sample", + "email": "sample@sample.com", + "phone": "+1 (777) 777-7777", + "address": "123 Main St", + "about": "Lorem ex proident ipsum ullamco velit sit nisi eiusmod cillum. Id tempor irure culpa nisi sit non qui veniam non ut. Aliquip reprehenderit excepteur mollit quis excepteur ex sit. Quis do eu veniam do ullamco occaecat eu cupidatat nisi laborum tempor minim fugiat pariatur. Ex in nulla ex velit.\r\n", + "registered": "2019-04-08T03:54:36 -01:00", + "latitude": -70.660321, + "longitude": 71.547525, + "tags": [ + "consequat", + "veniam", + "pariatur", + "aliqua", + "cillum", + "eu", + "officia" + ], + "friends": [ + { + "id": 0, + "name": "Test" + }, + { + "id": 1, + "name": "Sample" + }, + { + "id": 2, + "name": "Example" + } + ], + "greeting": "Hello, Sample! You have 6 unread messages.", + "favoriteFruit": "apple" + } +] From 09536cd6c838e647d5357042a7de84a925050531 Mon Sep 17 00:00:00 2001 From: Sean Leary Date: Sun, 15 Dec 2024 10:38:54 -0600 Subject: [PATCH 4/6] restore-jsonparserconfiguration: add jsonobject strict tests. Detect semicolon instead of colon separator in object --- src/main/java/org/json/JSONObject.java | 13 + .../junit/JSONParserConfigurationTest.java | 297 +- src/test/resources/compliantJsonObject.json | 3703 +++++++++++++++++ 3 files changed, 3975 insertions(+), 38 deletions(-) create mode 100644 src/test/resources/compliantJsonObject.json diff --git a/src/main/java/org/json/JSONObject.java b/src/main/java/org/json/JSONObject.java index 47d263a..f49bab3 100644 --- a/src/main/java/org/json/JSONObject.java +++ b/src/main/java/org/json/JSONObject.java @@ -268,8 +268,14 @@ public class JSONObject { switch (x.nextClean()) { case ';': + if (jsonParserConfiguration.isStrictMode()) { + throw x.syntaxError("Invalid character ';' found in object in strict mode"); + } case ',': if (x.nextClean() == '}') { + if (jsonParserConfiguration.isStrictMode()) { + throw x.syntaxError("Expected another object element"); + } return; } if (x.end()) { @@ -468,6 +474,13 @@ public class JSONObject { */ public JSONObject(String source, JSONParserConfiguration jsonParserConfiguration) throws JSONException { this(new JSONTokener(source), jsonParserConfiguration); + if (this.jsonParserConfiguration.isStrictMode()) { + char c = jsonTokener.nextClean(); + if (c != 0) { + throw jsonTokener.syntaxError(String.format("invalid character '%s' found after end of array", c)); + + } + } } /** diff --git a/src/test/java/org/json/junit/JSONParserConfigurationTest.java b/src/test/java/org/json/junit/JSONParserConfigurationTest.java index dbe436a..84c3911 100644 --- a/src/test/java/org/json/junit/JSONParserConfigurationTest.java +++ b/src/test/java/org/json/junit/JSONParserConfigurationTest.java @@ -4,7 +4,6 @@ import org.json.JSONArray; import org.json.JSONException; import org.json.JSONObject; import org.json.JSONParserConfiguration; -import org.junit.Ignore; import org.junit.Test; import java.io.IOException; @@ -54,19 +53,37 @@ public class JSONParserConfigurationTest { } @Test - public void givenInvalidInputArrays_testStrictModeTrue_shouldThrowJsonException() { + public void givenInvalidInput_testStrictModeTrue_shouldThrowJsonException() { JSONParserConfiguration jsonParserConfiguration = new JSONParserConfiguration() .withStrictMode(true); - - List strictModeInputTestCases = getNonCompliantJSONList(); - + List strictModeInputTestCases = getNonCompliantJSONArrayList(); // this is a lot easier to debug when things stop working for (int i = 0; i < strictModeInputTestCases.size(); ++i) { String testCase = strictModeInputTestCases.get(i); try { JSONArray jsonArray = new JSONArray(testCase, jsonParserConfiguration); String s = jsonArray.toString(); - System.out.println("Expected an exception, but got: " + s + " Noncompliant Array index: " + i); + String msg = "Expected an exception, but got: " + s + " Noncompliant Array index: " + i; + fail(msg); + } catch (Exception e) { + // its all good + } + } + } + + @Test + public void givenInvalidInputObjects_testStrictModeTrue_shouldThrowJsonException() { + JSONParserConfiguration jsonParserConfiguration = new JSONParserConfiguration() + .withStrictMode(true); + List strictModeInputTestCases = getNonCompliantJSONObjectList(); + // this is a lot easier to debug when things stop working + for (int i = 0; i < strictModeInputTestCases.size(); ++i) { + String testCase = strictModeInputTestCases.get(i); + try { + JSONObject jsonObject = new JSONObject(testCase, jsonParserConfiguration); + String s = jsonObject.toString(); + String msg = "Expected an exception, but got: " + s + " Noncompliant Array index: " + i; + fail(msg); } catch (Exception e) { // its all good } @@ -77,16 +94,22 @@ public class JSONParserConfigurationTest { public void givenEmptyArray_testStrictModeTrue_shouldNotThrowJsonException() { JSONParserConfiguration jsonParserConfiguration = new JSONParserConfiguration() .withStrictMode(true); - String testCase = "[]"; - JSONArray jsonArray = new JSONArray(testCase, jsonParserConfiguration); - assertEquals(testCase, jsonArray.toString()); } @Test - public void givenValidDoubleArray_testStrictModeTrue_shouldNotThrowJsonException() { + public void givenEmptyObject_testStrictModeTrue_shouldNotThrowJsonException() { + JSONParserConfiguration jsonParserConfiguration = new JSONParserConfiguration() + .withStrictMode(true); + String testCase = "{}"; + JSONObject jsonObject = new JSONObject(testCase, jsonParserConfiguration); + assertEquals(testCase, jsonObject.toString()); + } + + @Test + public void givenValidNestedArray_testStrictModeTrue_shouldNotThrowJsonException() { JSONParserConfiguration jsonParserConfiguration = new JSONParserConfiguration() .withStrictMode(true); @@ -103,48 +126,90 @@ public class JSONParserConfigurationTest { } @Test - public void givenValidEmptyArrayInsideArray_testStrictModeTrue_shouldNotThrowJsonException(){ + public void givenValidNestedObject_testStrictModeTrue_shouldNotThrowJsonException() { JSONParserConfiguration jsonParserConfiguration = new JSONParserConfiguration() .withStrictMode(true); + String testCase = "{\"a0\":[\"c\"], \"a1\":[10.2], \"a2\":[true, false, true]}"; + + JSONObject jsonObject = new JSONObject(testCase, jsonParserConfiguration); + JSONArray arrayShouldContainStringAt0 = jsonObject.getJSONArray("a0"); + JSONArray arrayShouldContainNumberAt0 = jsonObject.getJSONArray("a1"); + JSONArray arrayShouldContainBooleanAt0 = jsonObject.getJSONArray("a2"); + + assertTrue(arrayShouldContainStringAt0.get(0) instanceof String); + assertTrue(arrayShouldContainNumberAt0.get(0) instanceof Number); + assertTrue(arrayShouldContainBooleanAt0.get(0) instanceof Boolean); + } + + @Test + public void givenValidEmptyArrayInsideArray_testStrictModeTrue_shouldNotThrowJsonException(){ + JSONParserConfiguration jsonParserConfiguration = new JSONParserConfiguration() + .withStrictMode(true); String testCase = "[[]]"; - JSONArray jsonArray = new JSONArray(testCase, jsonParserConfiguration); - assertEquals(testCase, jsonArray.toString()); } + @Test + public void givenValidEmptyArrayInsideObject_testStrictModeTrue_shouldNotThrowJsonException(){ + JSONParserConfiguration jsonParserConfiguration = new JSONParserConfiguration() + .withStrictMode(true); + String testCase = "{\"a0\":[]}"; + JSONObject jsonObject = new JSONObject(testCase, jsonParserConfiguration); + assertEquals(testCase, jsonObject.toString()); + } + @Test public void givenValidEmptyArrayInsideArray_testStrictModeFalse_shouldNotThrowJsonException(){ JSONParserConfiguration jsonParserConfiguration = new JSONParserConfiguration() .withStrictMode(false); - String testCase = "[[]]"; - JSONArray jsonArray = new JSONArray(testCase, jsonParserConfiguration); - assertEquals(testCase, jsonArray.toString()); } @Test - public void givenInvalidString_testStrictModeTrue_shouldThrowJsonException() { + public void givenValidEmptyArrayInsideObject_testStrictModeFalse_shouldNotThrowJsonException(){ + JSONParserConfiguration jsonParserConfiguration = new JSONParserConfiguration() + .withStrictMode(false); + String testCase = "{\"a0\":[]}"; + JSONObject jsonObject = new JSONObject(testCase, jsonParserConfiguration); + assertEquals(testCase, jsonObject.toString()); + } + + @Test + public void givenInvalidStringArray_testStrictModeTrue_shouldThrowJsonException() { JSONParserConfiguration jsonParserConfiguration = new JSONParserConfiguration() .withStrictMode(true); - String testCase = "[badString]"; - JSONException je = assertThrows(JSONException.class, () -> new JSONArray(testCase, jsonParserConfiguration)); - assertEquals("Value 'badString' is not surrounded by quotes at 10 [character 11 line 1]", je.getMessage()); } @Test - public void allowNullInStrictMode() { + public void givenInvalidStringObject_testStrictModeTrue_shouldThrowJsonException() { + JSONParserConfiguration jsonParserConfiguration = new JSONParserConfiguration() + .withStrictMode(true); + String testCase = "{\"a0\":badString}"; + JSONException je = assertThrows(JSONException.class, () -> new JSONObject(testCase, jsonParserConfiguration)); + assertEquals("Value 'badString' is not surrounded by quotes at 15 [character 16 line 1]", je.getMessage()); + } + + @Test + public void allowNullArrayInStrictMode() { String expected = "[null]"; JSONArray jsonArray = new JSONArray(expected, new JSONParserConfiguration().withStrictMode(true)); assertEquals(expected, jsonArray.toString()); } + @Test + public void allowNullObjectInStrictMode() { + String expected = "{\"a0\":null}"; + JSONObject jsonObject = new JSONObject(expected, new JSONParserConfiguration().withStrictMode(true)); + assertEquals(expected, jsonObject.toString()); + } + @Test public void shouldHandleNumericArray() { String expected = "[10]"; @@ -152,16 +217,31 @@ public class JSONParserConfigurationTest { assertEquals(expected, jsonArray.toString()); } + @Test + public void shouldHandleNumericObject() { + String expected = "{\"a0\":10}"; + JSONObject jsonObject = new JSONObject(expected, new JSONParserConfiguration().withStrictMode(true)); + assertEquals(expected, jsonObject.toString()); + } @Test public void givenCompliantJSONArrayFile_testStrictModeTrue_shouldNotThrowAnyException() throws IOException { try (Stream lines = Files.lines(Paths.get("src/test/resources/compliantJsonArray.json"))) { String compliantJsonArrayAsString = lines.collect(Collectors.joining()); JSONParserConfiguration jsonParserConfiguration = new JSONParserConfiguration() .withStrictMode(true); - new JSONArray(compliantJsonArrayAsString, jsonParserConfiguration); } + } + @Test + public void givenCompliantJSONObjectFile_testStrictModeTrue_shouldNotThrowAnyException() throws IOException { + try (Stream lines = Files.lines(Paths.get("src/test/resources/compliantJsonObject.json"))) { + String compliantJsonObjectAsString = lines.collect(Collectors.joining()); + JSONParserConfiguration jsonParserConfiguration = new JSONParserConfiguration() + .withStrictMode(true); + + new JSONObject(compliantJsonObjectAsString, jsonParserConfiguration); + } } @Test @@ -169,7 +249,7 @@ public class JSONParserConfigurationTest { JSONParserConfiguration jsonParserConfiguration = new JSONParserConfiguration() .withStrictMode(false); - List strictModeInputTestCases = getNonCompliantJSONList(); + List strictModeInputTestCases = getNonCompliantJSONArrayList(); // this is a lot easier to debug when things stop working for (int i = 0; i < strictModeInputTestCases.size(); ++i) { @@ -178,62 +258,108 @@ public class JSONParserConfigurationTest { JSONArray jsonArray = new JSONArray(testCase, jsonParserConfiguration); } catch (Exception e) { System.out.println("Unexpected exception: " + e.getMessage() + " Noncompliant Array index: " + i); + fail(String.format("Noncompliant array index: %d", i)); + } + } + } + + @Test + public void givenInvalidInputObjects_testStrictModeFalse_shouldNotThrowAnyException() { + JSONParserConfiguration jsonParserConfiguration = new JSONParserConfiguration() + .withStrictMode(false); + + List strictModeInputTestCases = getNonCompliantJSONObjectList(); + + // this is a lot easier to debug when things stop working + for (int i = 0; i < strictModeInputTestCases.size(); ++i) { + String testCase = strictModeInputTestCases.get(i); + try { + JSONObject jsonObject = new JSONObject(testCase, jsonParserConfiguration); + } catch (Exception e) { + System.out.println("Unexpected exception: " + e.getMessage() + " Noncompliant Array index: " + i); + fail(String.format("Noncompliant array index: %d", i)); } } - strictModeInputTestCases.forEach(testCase -> new JSONArray(testCase, jsonParserConfiguration)); } @Test public void givenInvalidInputArray_testStrictModeTrue_shouldThrowInvalidCharacterErrorMessage() { JSONParserConfiguration jsonParserConfiguration = new JSONParserConfiguration() .withStrictMode(true); - String testCase = "[1,2];[3,4]"; - JSONException je = assertThrows("expected non-compliant array but got instead: " + testCase, JSONException.class, () -> new JSONArray(testCase, jsonParserConfiguration)); - assertEquals("invalid character ';' found after end of array at 6 [character 7 line 1]", je.getMessage()); } + @Test + public void givenInvalidInputObject_testStrictModeTrue_shouldThrowInvalidCharacterErrorMessage() { + JSONParserConfiguration jsonParserConfiguration = new JSONParserConfiguration() + .withStrictMode(true); + String testCase = "{\"a0\":[1,2];\"a1\":[3,4]}"; + JSONException je = assertThrows("expected non-compliant array but got instead: " + testCase, + JSONException.class, () -> new JSONObject(testCase, jsonParserConfiguration)); + assertEquals("Invalid character ';' found in object in strict mode at 12 [character 13 line 1]", je.getMessage()); + } + @Test public void givenInvalidInputArrayWithNumericStrings_testStrictModeTrue_shouldThrowInvalidCharacterErrorMessage() { JSONParserConfiguration jsonParserConfiguration = new JSONParserConfiguration() .withStrictMode(true); - String testCase = "[\"1\",\"2\"];[3,4]"; - JSONException je = assertThrows("expected non-compliant array but got instead: " + testCase, JSONException.class, () -> new JSONArray(testCase, jsonParserConfiguration)); - assertEquals("invalid character ';' found after end of array at 10 [character 11 line 1]", je.getMessage()); } + @Test + public void givenInvalidInputObjectWithNumericStrings_testStrictModeTrue_shouldThrowInvalidCharacterErrorMessage() { + JSONParserConfiguration jsonParserConfiguration = new JSONParserConfiguration() + .withStrictMode(true); + String testCase = "{\"a0\":[\"1\",\"2\"];\"a1\":[3,4]}"; + JSONException je = assertThrows("expected non-compliant array but got instead: " + testCase, + JSONException.class, () -> new JSONObject(testCase, jsonParserConfiguration)); + assertEquals("Invalid character ';' found in object in strict mode at 16 [character 17 line 1]", je.getMessage()); + } + @Test public void givenInvalidInputArray_testStrictModeTrue_shouldThrowValueNotSurroundedByQuotesErrorMessage() { JSONParserConfiguration jsonParserConfiguration = new JSONParserConfiguration() .withStrictMode(true); - String testCase = "[{\"test\": implied}]"; - JSONException je = assertThrows("expected non-compliant array but got instead: " + testCase, JSONException.class, () -> new JSONArray(testCase, jsonParserConfiguration)); - assertEquals("Value 'implied' is not surrounded by quotes at 17 [character 18 line 1]", je.getMessage()); } + @Test + public void givenInvalidInputObject_testStrictModeTrue_shouldThrowValueNotSurroundedByQuotesErrorMessage() { + JSONParserConfiguration jsonParserConfiguration = new JSONParserConfiguration() + .withStrictMode(true); + String testCase = "{\"a0\":{\"test\": implied}]}"; + JSONException je = assertThrows("expected non-compliant array but got instead: " + testCase, + JSONException.class, () -> new JSONObject(testCase, jsonParserConfiguration)); + assertEquals("Value 'implied' is not surrounded by quotes at 22 [character 23 line 1]", je.getMessage()); + } + @Test public void givenInvalidInputArray_testStrictModeFalse_shouldNotThrowAnyException() { JSONParserConfiguration jsonParserConfiguration = new JSONParserConfiguration() .withStrictMode(false); - String testCase = "[{\"test\": implied}]"; - new JSONArray(testCase, jsonParserConfiguration); } @Test - public void givenNonCompliantQuotes_testStrictModeTrue_shouldThrowJsonExceptionWithConcreteErrorDescription() { + public void givenInvalidInputObject_testStrictModeFalse_shouldNotThrowAnyException() { + JSONParserConfiguration jsonParserConfiguration = new JSONParserConfiguration() + .withStrictMode(false); + String testCase = "{\"a0\":{\"test\": implied}}"; + new JSONObject(testCase, jsonParserConfiguration); + } + + @Test + public void givenNonCompliantQuotesArray_testStrictModeTrue_shouldThrowJsonExceptionWithConcreteErrorDescription() { JSONParserConfiguration jsonParserConfiguration = new JSONParserConfiguration() .withStrictMode(true); @@ -266,7 +392,40 @@ public class JSONParserConfigurationTest { } @Test - public void givenUnbalancedQuotes_testStrictModeFalse_shouldThrowJsonException() { + public void givenNonCompliantQuotesObject_testStrictModeTrue_shouldThrowJsonExceptionWithConcreteErrorDescription() { + JSONParserConfiguration jsonParserConfiguration = new JSONParserConfiguration() + .withStrictMode(true); + + String testCaseOne = "{\"abc': \"test\"}"; + String testCaseTwo = "{'abc\": \"test\"}"; + String testCaseThree = "{\"a\":'abc'}"; + String testCaseFour = "{'testField': \"testValue\"}"; + + JSONException jeOne = assertThrows(JSONException.class, + () -> new JSONObject(testCaseOne, jsonParserConfiguration)); + JSONException jeTwo = assertThrows(JSONException.class, + () -> new JSONObject(testCaseTwo, jsonParserConfiguration)); + JSONException jeThree = assertThrows(JSONException.class, + () -> new JSONObject(testCaseThree, jsonParserConfiguration)); + JSONException jeFour = assertThrows(JSONException.class, + () -> new JSONObject(testCaseFour, jsonParserConfiguration)); + + assertEquals( + "Expected a ':' after a key at 10 [character 11 line 1]", + jeOne.getMessage()); + assertEquals( + "Single quote wrap not allowed in strict mode at 2 [character 3 line 1]", + jeTwo.getMessage()); + assertEquals( + "Single quote wrap not allowed in strict mode at 6 [character 7 line 1]", + jeThree.getMessage()); + assertEquals( + "Single quote wrap not allowed in strict mode at 2 [character 3 line 1]", + jeFour.getMessage()); + } + + @Test + public void givenUnbalancedQuotesArray_testStrictModeFalse_shouldThrowJsonException() { JSONParserConfiguration jsonParserConfiguration = new JSONParserConfiguration() .withStrictMode(false); @@ -282,6 +441,22 @@ public class JSONParserConfigurationTest { assertEquals("Unterminated string. Character with int code 0 is not allowed within a quoted string. at 15 [character 16 line 1]", jeTwo.getMessage()); } + @Test + public void givenUnbalancedQuotesObject_testStrictModeFalse_shouldThrowJsonException() { + JSONParserConfiguration jsonParserConfiguration = new JSONParserConfiguration() + .withStrictMode(false); + + String testCaseOne = "{\"abc': \"test\"}"; + String testCaseTwo = "{'abc\": \"test\"}"; + + JSONException jeOne = assertThrows(JSONException.class, + () -> new JSONObject(testCaseOne, jsonParserConfiguration)); + JSONException jeTwo = assertThrows(JSONException.class, + () -> new JSONObject(testCaseTwo, jsonParserConfiguration)); + + assertEquals("Expected a ':' after a key at 10 [character 11 line 1]", jeOne.getMessage()); + assertEquals("Unterminated string. Character with int code 0 is not allowed within a quoted string. at 15 [character 16 line 1]", jeTwo.getMessage()); + } @Test public void givenInvalidInputArray_testStrictModeTrue_shouldThrowKeyNotSurroundedByQuotesErrorMessage() { @@ -295,13 +470,25 @@ public class JSONParserConfigurationTest { assertEquals("Value 'test' is not surrounded by quotes at 6 [character 7 line 1]", je.getMessage()); } + @Test + public void givenInvalidInputObject_testStrictModeTrue_shouldThrowKeyNotSurroundedByQuotesErrorMessage() { + JSONParserConfiguration jsonParserConfiguration = new JSONParserConfiguration() + .withStrictMode(true); + + String testCase = "{test: implied}"; + JSONException je = assertThrows("expected non-compliant json but got instead: " + testCase, + JSONException.class, () -> new JSONObject(testCase, jsonParserConfiguration)); + + assertEquals("Value 'test' is not surrounded by quotes at 5 [character 6 line 1]", je.getMessage()); + } + /** * This method contains short but focused use-case samples and is exclusively used to test strictMode unit tests in * this class. * * @return List with JSON strings. */ - private List getNonCompliantJSONList() { + private List getNonCompliantJSONArrayList() { return Arrays.asList( "[1],", "[1,]", @@ -326,4 +513,38 @@ public class JSONParserConfigurationTest { "[{\"number\":\"7990154836330\",\"color\":'c'},{\"number\":8784148854580,\"color\":RosyBrown},{\"number\":\"5875770107113\",\"color\":\"DarkSeaGreen\"}]", "[{test: \"implied\"}]"); } + + /** + * This method contains short but focused use-case samples and is exclusively used to test strictMode unit tests in + * this class. + * + * @return List with JSON strings. + */ + private List getNonCompliantJSONObjectList() { + return Arrays.asList( + "{\"a\":1},", + "{\"a\":1,}", + "{\"a0\":[1],\"a1\":\"sa\",\"a2\":[2]}a", + "{\"a\":1},\"dsa\": \"test\"", + "{\"a\":[a]}", + "{}asdf", + "{}}", + "{}]", + "{}{", + "{}[", + "{},", + "{}:", + "{},{", + "{},[", + "{\"a0\":[1,2];\"a1\":[3,4]}", + "{\"a\":test}", + "{a:{'testSingleQuote': 'testSingleQuote'}}", + "{\"a0\":1, \"a1\":2,\"a2\":3}:{\"a3\":4,\"a4\":5}", + "{\"a\":{test: implied}}", + "{a:{\"test\": implied}}", + "{a:[{\"number\":\"7990154836330\",\"color\":'c'},{\"number\":8784148854580,\"color\":RosyBrown},{\"number\":\"5875770107113\",\"color\":\"DarkSeaGreen\"}]}", + "{a:{test: \"implied\"}}" + ); + } + } diff --git a/src/test/resources/compliantJsonObject.json b/src/test/resources/compliantJsonObject.json new file mode 100644 index 0000000..cb2918d --- /dev/null +++ b/src/test/resources/compliantJsonObject.json @@ -0,0 +1,3703 @@ +{ + "a0": [ + { + "id": 0, + "name": "Elijah", + "city": "Austin", + "age": 78, + "friends": [ + { + "name": "Michelle", + "hobbies": [ + "Watching Sports", + "Reading", + "Skiing & Snowboarding" + ] + }, + { + "name": "Robert", + "hobbies": [ + "Traveling", + "Video Games" + ] + } + ] + }, + { + "id": 1, + "name": "Noah", + "city": "Boston", + "age": 97, + "friends": [ + { + "name": "Oliver", + "hobbies": [ + "Watching Sports", + "Skiing & Snowboarding", + "Collecting" + ] + }, + { + "name": "Olivia", + "hobbies": [ + "Running", + "Music", + "Woodworking" + ] + }, + { + "name": "Robert", + "hobbies": [ + "Woodworking", + "Calligraphy", + "Genealogy" + ] + }, + { + "name": "Ava", + "hobbies": [ + "Walking", + "Church Activities" + ] + }, + { + "name": "Michael", + "hobbies": [ + "Music", + "Church Activities" + ] + }, + { + "name": "Michael", + "hobbies": [ + "Martial Arts", + "Painting", + "Jewelry Making" + ] + } + ] + }, + { + "id": 2, + "name": "Evy", + "city": "San Diego", + "age": 48, + "friends": [ + { + "name": "Joe", + "hobbies": [ + "Reading", + "Volunteer Work" + ] + }, + { + "name": "Joe", + "hobbies": [ + "Genealogy", + "Golf" + ] + }, + { + "name": "Oliver", + "hobbies": [ + "Collecting", + "Writing", + "Bicycling" + ] + }, + { + "name": "Liam", + "hobbies": [ + "Church Activities", + "Jewelry Making" + ] + }, + { + "name": "Amelia", + "hobbies": [ + "Calligraphy", + "Dancing" + ] + } + ] + }, + { + "id": 3, + "name": "Oliver", + "city": "St. Louis", + "age": 39, + "friends": [ + { + "name": "Mateo", + "hobbies": [ + "Watching Sports", + "Gardening" + ] + }, + { + "name": "Nora", + "hobbies": [ + "Traveling", + "Team Sports" + ] + }, + { + "name": "Ava", + "hobbies": [ + "Church Activities", + "Running" + ] + }, + { + "name": "Amelia", + "hobbies": [ + "Gardening", + "Board Games", + "Watching Sports" + ] + }, + { + "name": "Leo", + "hobbies": [ + "Martial Arts", + "Video Games", + "Reading" + ] + } + ] + }, + { + "id": 4, + "name": "Michael", + "city": "St. Louis", + "age": 95, + "friends": [ + { + "name": "Mateo", + "hobbies": [ + "Movie Watching", + "Collecting" + ] + }, + { + "name": "Chris", + "hobbies": [ + "Housework", + "Bicycling", + "Collecting" + ] + } + ] + }, + { + "id": 5, + "name": "Michael", + "city": "Portland", + "age": 19, + "friends": [ + { + "name": "Jack", + "hobbies": [ + "Painting", + "Television" + ] + }, + { + "name": "Oliver", + "hobbies": [ + "Walking", + "Watching Sports", + "Movie Watching" + ] + }, + { + "name": "Charlotte", + "hobbies": [ + "Podcasts", + "Jewelry Making" + ] + }, + { + "name": "Elijah", + "hobbies": [ + "Eating Out", + "Painting" + ] + } + ] + }, + { + "id": 6, + "name": "Lucas", + "city": "Austin", + "age": 76, + "friends": [ + { + "name": "John", + "hobbies": [ + "Genealogy", + "Cooking" + ] + }, + { + "name": "John", + "hobbies": [ + "Socializing", + "Yoga" + ] + } + ] + }, + { + "id": 7, + "name": "Michelle", + "city": "San Antonio", + "age": 25, + "friends": [ + { + "name": "Jack", + "hobbies": [ + "Music", + "Golf" + ] + }, + { + "name": "Daniel", + "hobbies": [ + "Socializing", + "Housework", + "Walking" + ] + }, + { + "name": "Robert", + "hobbies": [ + "Collecting", + "Walking" + ] + }, + { + "name": "Nora", + "hobbies": [ + "Painting", + "Church Activities" + ] + }, + { + "name": "Mia", + "hobbies": [ + "Running", + "Painting" + ] + } + ] + }, + { + "id": 8, + "name": "Emily", + "city": "Austin", + "age": 61, + "friends": [ + { + "name": "Nora", + "hobbies": [ + "Bicycling", + "Skiing & Snowboarding", + "Watching Sports" + ] + }, + { + "name": "Ava", + "hobbies": [ + "Writing", + "Reading", + "Collecting" + ] + }, + { + "name": "Amelia", + "hobbies": [ + "Eating Out", + "Watching Sports" + ] + }, + { + "name": "Daniel", + "hobbies": [ + "Skiing & Snowboarding", + "Martial Arts", + "Writing" + ] + }, + { + "name": "Zoey", + "hobbies": [ + "Board Games", + "Tennis" + ] + } + ] + }, + { + "id": 9, + "name": "Liam", + "city": "New Orleans", + "age": 33, + "friends": [ + { + "name": "Chloe", + "hobbies": [ + "Traveling", + "Bicycling", + "Shopping" + ] + }, + { + "name": "Evy", + "hobbies": [ + "Eating Out", + "Watching Sports" + ] + }, + { + "name": "Grace", + "hobbies": [ + "Jewelry Making", + "Yoga", + "Podcasts" + ] + } + ] + }, + { + "id": 10, + "name": "Levi", + "city": "New Orleans", + "age": 59, + "friends": [ + { + "name": "Noah", + "hobbies": [ + "Video Games", + "Fishing", + "Shopping" + ] + }, + { + "name": "Sarah", + "hobbies": [ + "Woodworking", + "Music", + "Reading" + ] + } + ] + }, + { + "id": 11, + "name": "Lucas", + "city": "Portland", + "age": 82, + "friends": [ + { + "name": "Luke", + "hobbies": [ + "Jewelry Making", + "Yoga" + ] + }, + { + "name": "Noah", + "hobbies": [ + "Fishing", + "Movie Watching" + ] + }, + { + "name": "Evy", + "hobbies": [ + "Gardening", + "Church Activities", + "Fishing" + ] + } + ] + }, + { + "id": 12, + "name": "Kevin", + "city": "Charleston", + "age": 82, + "friends": [ + { + "name": "Oliver", + "hobbies": [ + "Eating Out" + ] + }, + { + "name": "Michael", + "hobbies": [ + "Fishing", + "Writing" + ] + } + ] + }, + { + "id": 13, + "name": "Olivia", + "city": "San Antonio", + "age": 34, + "friends": [ + { + "name": "Daniel", + "hobbies": [ + "Yoga", + "Traveling", + "Movie Watching" + ] + }, + { + "name": "Zoey", + "hobbies": [ + "Team Sports", + "Writing" + ] + } + ] + }, + { + "id": 14, + "name": "Robert", + "city": "Los Angeles", + "age": 49, + "friends": [ + { + "name": "Michelle", + "hobbies": [ + "Yoga", + "Television" + ] + }, + { + "name": "Daniel", + "hobbies": [ + "Fishing", + "Martial Arts" + ] + }, + { + "name": "Chloe", + "hobbies": [ + "Church Activities", + "Television" + ] + }, + { + "name": "Sarah", + "hobbies": [ + "Movie Watching", + "Playing Cards" + ] + }, + { + "name": "Kevin", + "hobbies": [ + "Golf", + "Running", + "Cooking" + ] + } + ] + }, + { + "id": 15, + "name": "Grace", + "city": "Chicago", + "age": 98, + "friends": [ + { + "name": "Joe", + "hobbies": [ + "Traveling", + "Genealogy" + ] + }, + { + "name": "Mateo", + "hobbies": [ + "Golf", + "Podcasts" + ] + }, + { + "name": "Mateo", + "hobbies": [ + "Reading", + "Cooking" + ] + } + ] + }, + { + "id": 16, + "name": "Michael", + "city": "New Orleans", + "age": 78, + "friends": [ + { + "name": "Amelia", + "hobbies": [ + "Running", + "Housework", + "Gardening" + ] + }, + { + "name": "Michael", + "hobbies": [ + "Writing", + "Golf" + ] + }, + { + "name": "Leo", + "hobbies": [ + "Running", + "Church Activities" + ] + }, + { + "name": "Elijah", + "hobbies": [ + "Volunteer Work", + "Eating Out" + ] + }, + { + "name": "Mateo", + "hobbies": [ + "Socializing", + "Watching Sports", + "Collecting" + ] + }, + { + "name": "Levi", + "hobbies": [ + "Eating Out", + "Walking" + ] + } + ] + }, + { + "id": 17, + "name": "Mateo", + "city": "Palm Springs", + "age": 19, + "friends": [ + { + "name": "Emily", + "hobbies": [ + "Playing Cards", + "Walking" + ] + }, + { + "name": "Sophie", + "hobbies": [ + "Gardening", + "Board Games", + "Volunteer Work" + ] + }, + { + "name": "Camila", + "hobbies": [ + "Board Games", + "Dancing" + ] + }, + { + "name": "John", + "hobbies": [ + "Golf", + "Playing Cards", + "Music" + ] + } + ] + }, + { + "id": 18, + "name": "Levi", + "city": "Chicago", + "age": 38, + "friends": [ + { + "name": "Emma", + "hobbies": [ + "Tennis", + "Eating Out" + ] + }, + { + "name": "Emma", + "hobbies": [ + "Writing", + "Reading", + "Eating Out" + ] + }, + { + "name": "Daniel", + "hobbies": [ + "Collecting", + "Video Games" + ] + }, + { + "name": "Evy", + "hobbies": [ + "Shopping", + "Walking" + ] + }, + { + "name": "Sophie", + "hobbies": [ + "Dancing", + "Volunteer Work" + ] + }, + { + "name": "Mia", + "hobbies": [ + "Podcasts", + "Woodworking", + "Martial Arts" + ] + } + ] + }, + { + "id": 19, + "name": "Luke", + "city": "New York City", + "age": 49, + "friends": [ + { + "name": "Leo", + "hobbies": [ + "Writing", + "Playing Cards", + "Housework" + ] + }, + { + "name": "Emma", + "hobbies": [ + "Gardening", + "Running" + ] + }, + { + "name": "Sarah", + "hobbies": [ + "Golf", + "Music" + ] + }, + { + "name": "Jack", + "hobbies": [ + "Board Games", + "Socializing", + "Writing" + ] + }, + { + "name": "Jack", + "hobbies": [ + "Movie Watching", + "Writing", + "Fishing" + ] + }, + { + "name": "Michael", + "hobbies": [ + "Golf", + "Jewelry Making", + "Yoga" + ] + } + ] + }, + { + "id": 20, + "name": "Camila", + "city": "New Orleans", + "age": 69, + "friends": [ + { + "name": "Kevin", + "hobbies": [ + "Video Games", + "Collecting", + "Painting" + ] + }, + { + "name": "Grace", + "hobbies": [ + "Reading", + "Volunteer Work" + ] + } + ] + }, + { + "id": 21, + "name": "Amelia", + "city": "Charleston", + "age": 70, + "friends": [ + { + "name": "John", + "hobbies": [ + "Quilting", + "Volunteer Work" + ] + }, + { + "name": "Leo", + "hobbies": [ + "Painting", + "Podcasts" + ] + } + ] + }, + { + "id": 22, + "name": "Victoria", + "city": "Miami Beach", + "age": 50, + "friends": [ + { + "name": "Mia", + "hobbies": [ + "Cooking", + "Team Sports" + ] + }, + { + "name": "Lucas", + "hobbies": [ + "Team Sports", + "Genealogy" + ] + } + ] + }, + { + "id": 23, + "name": "Kevin", + "city": "Miami Beach", + "age": 93, + "friends": [ + { + "name": "Jack", + "hobbies": [ + "Bicycling", + "Fishing" + ] + }, + { + "name": "Sophie", + "hobbies": [ + "Martial Arts", + "Genealogy", + "Tennis" + ] + }, + { + "name": "Chloe", + "hobbies": [ + "Yoga" + ] + } + ] + }, + { + "id": 24, + "name": "Daniel", + "city": "Saint Augustine", + "age": 43, + "friends": [ + { + "name": "Sarah", + "hobbies": [ + "Calligraphy", + "Martial Arts", + "Music" + ] + }, + { + "name": "Evy", + "hobbies": [ + "Walking", + "Bicycling" + ] + }, + { + "name": "Kevin", + "hobbies": [ + "Collecting", + "Golf" + ] + }, + { + "name": "Mia", + "hobbies": [ + "Podcasts", + "Walking" + ] + } + ] + }, + { + "id": 25, + "name": "Olivia", + "city": "Austin", + "age": 46, + "friends": [ + { + "name": "Mia", + "hobbies": [ + "Podcasts", + "Housework" + ] + }, + { + "name": "Robert", + "hobbies": [ + "Golf", + "Volunteer Work" + ] + }, + { + "name": "Michelle", + "hobbies": [ + "Eating Out", + "Music" + ] + }, + { + "name": "Elijah", + "hobbies": [ + "Eating Out", + "Genealogy", + "Reading" + ] + } + ] + }, + { + "id": 26, + "name": "Michael", + "city": "Palm Springs", + "age": 62, + "friends": [ + { + "name": "Sarah", + "hobbies": [ + "Socializing", + "Playing Cards" + ] + }, + { + "name": "Daniel", + "hobbies": [ + "Playing Cards", + "Shopping", + "Collecting" + ] + }, + { + "name": "Chloe", + "hobbies": [ + "Music", + "Movie Watching" + ] + }, + { + "name": "Zoey", + "hobbies": [ + "Volunteer Work", + "Calligraphy", + "Jewelry Making" + ] + } + ] + }, + { + "id": 27, + "name": "Kevin", + "city": "San Antonio", + "age": 97, + "friends": [ + { + "name": "Sarah", + "hobbies": [ + "Television", + "Quilting", + "Team Sports" + ] + }, + { + "name": "Oliver", + "hobbies": [ + "Shopping", + "Martial Arts" + ] + } + ] + }, + { + "id": 28, + "name": "Oliver", + "city": "Honolulu", + "age": 79, + "friends": [ + { + "name": "Charlotte", + "hobbies": [ + "Housework", + "Jewelry Making" + ] + }, + { + "name": "Isabella", + "hobbies": [ + "Volunteer Work", + "Movie Watching" + ] + }, + { + "name": "Ava", + "hobbies": [ + "Traveling", + "Bicycling" + ] + }, + { + "name": "Chris", + "hobbies": [ + "Shopping", + "Church Activities", + "Dancing" + ] + }, + { + "name": "Sarah", + "hobbies": [ + "Reading", + "Movie Watching" + ] + }, + { + "name": "Daniel", + "hobbies": [ + "Socializing", + "Collecting", + "Cooking" + ] + } + ] + }, + { + "id": 29, + "name": "Levi", + "city": "Miami Beach", + "age": 46, + "friends": [ + { + "name": "Ava", + "hobbies": [ + "Housework", + "Video Games", + "Walking" + ] + }, + { + "name": "Elijah", + "hobbies": [ + "Golf", + "Volunteer Work", + "Painting" + ] + }, + { + "name": "Lucas", + "hobbies": [ + "Writing", + "Martial Arts", + "Television" + ] + } + ] + }, + { + "id": 30, + "name": "Michael", + "city": "Seattle", + "age": 18, + "friends": [ + { + "name": "Oliver", + "hobbies": [ + "Shopping", + "Woodworking" + ] + }, + { + "name": "Emma", + "hobbies": [ + "Yoga", + "Genealogy", + "Traveling" + ] + }, + { + "name": "Evy", + "hobbies": [ + "Eating Out", + "Church Activities", + "Calligraphy" + ] + }, + { + "name": "Noah", + "hobbies": [ + "Board Games", + "Television" + ] + } + ] + }, + { + "id": 31, + "name": "Isabella", + "city": "Savannah", + "age": 65, + "friends": [ + { + "name": "Jack", + "hobbies": [ + "Church Activities", + "Housework", + "Martial Arts" + ] + }, + { + "name": "Oliver", + "hobbies": [ + "Calligraphy", + "Cooking" + ] + }, + { + "name": "Grace", + "hobbies": [ + "Volunteer Work", + "Podcasts", + "Tennis" + ] + } + ] + }, + { + "id": 32, + "name": "Chris", + "city": "Las Vegas", + "age": 31, + "friends": [ + { + "name": "Levi", + "hobbies": [ + "Shopping", + "Fishing" + ] + }, + { + "name": "Nora", + "hobbies": [ + "Dancing", + "Quilting" + ] + }, + { + "name": "Levi", + "hobbies": [ + "Reading", + "Eating Out" + ] + }, + { + "name": "Zoey", + "hobbies": [ + "Traveling", + "Golf", + "Genealogy" + ] + }, + { + "name": "Mia", + "hobbies": [ + "Video Games", + "Shopping", + "Walking" + ] + } + ] + }, + { + "id": 33, + "name": "Kevin", + "city": "Portland", + "age": 51, + "friends": [ + { + "name": "Olivia", + "hobbies": [ + "Running", + "Calligraphy" + ] + }, + { + "name": "Amelia", + "hobbies": [ + "Tennis", + "Genealogy" + ] + } + ] + }, + { + "id": 34, + "name": "Sophie", + "city": "New York City", + "age": 25, + "friends": [ + { + "name": "Levi", + "hobbies": [ + "Video Games", + "Board Games", + "Podcasts" + ] + }, + { + "name": "Joe", + "hobbies": [ + "Calligraphy", + "Video Games", + "Martial Arts" + ] + } + ] + }, + { + "id": 35, + "name": "John", + "city": "Orlando", + "age": 67, + "friends": [ + { + "name": "Nora", + "hobbies": [ + "Podcasts", + "Skiing & Snowboarding", + "Quilting" + ] + }, + { + "name": "Jack", + "hobbies": [ + "Tennis", + "Socializing", + "Music" + ] + }, + { + "name": "Camila", + "hobbies": [ + "Walking", + "Church Activities", + "Quilting" + ] + }, + { + "name": "Grace", + "hobbies": [ + "Team Sports", + "Cooking" + ] + }, + { + "name": "Isabella", + "hobbies": [ + "Skiing & Snowboarding", + "Dancing", + "Painting" + ] + }, + { + "name": "Ava", + "hobbies": [ + "Tennis", + "Bicycling" + ] + } + ] + }, + { + "id": 36, + "name": "Emily", + "city": "New York City", + "age": 82, + "friends": [ + { + "name": "Emma", + "hobbies": [ + "Church Activities", + "Writing" + ] + }, + { + "name": "Luke", + "hobbies": [ + "Running", + "Calligraphy", + "Tennis" + ] + }, + { + "name": "Robert", + "hobbies": [ + "Dancing", + "Socializing" + ] + }, + { + "name": "Amelia", + "hobbies": [ + "Genealogy", + "Calligraphy", + "Tennis" + ] + } + ] + }, + { + "id": 37, + "name": "Amelia", + "city": "New Orleans", + "age": 28, + "friends": [ + { + "name": "Victoria", + "hobbies": [ + "Traveling", + "Skiing & Snowboarding" + ] + }, + { + "name": "Luke", + "hobbies": [ + "Martial Arts", + "Cooking", + "Dancing" + ] + }, + { + "name": "Olivia", + "hobbies": [ + "Bicycling", + "Walking", + "Podcasts" + ] + }, + { + "name": "Elijah", + "hobbies": [ + "Traveling", + "Volunteer Work", + "Collecting" + ] + } + ] + }, + { + "id": 38, + "name": "Victoria", + "city": "Austin", + "age": 71, + "friends": [ + { + "name": "Noah", + "hobbies": [ + "Yoga", + "Shopping" + ] + }, + { + "name": "Evy", + "hobbies": [ + "Eating Out", + "Writing", + "Shopping" + ] + }, + { + "name": "Camila", + "hobbies": [ + "Volunteer Work", + "Team Sports" + ] + }, + { + "name": "Lucas", + "hobbies": [ + "Volunteer Work", + "Board Games", + "Running" + ] + } + ] + }, + { + "id": 39, + "name": "Mia", + "city": "Honolulu", + "age": 63, + "friends": [ + { + "name": "Sophie", + "hobbies": [ + "Volunteer Work", + "Housework", + "Bicycling" + ] + }, + { + "name": "Evy", + "hobbies": [ + "Woodworking", + "Golf" + ] + }, + { + "name": "Ava", + "hobbies": [ + "Martial Arts", + "Skiing & Snowboarding", + "Dancing" + ] + }, + { + "name": "Joe", + "hobbies": [ + "Collecting", + "Watching Sports" + ] + }, + { + "name": "Camila", + "hobbies": [ + "Television", + "Socializing", + "Skiing & Snowboarding" + ] + }, + { + "name": "Robert", + "hobbies": [ + "Martial Arts", + "Woodworking", + "Reading" + ] + } + ] + }, + { + "id": 40, + "name": "Daniel", + "city": "Las Vegas", + "age": 50, + "friends": [ + { + "name": "Kevin", + "hobbies": [ + "Bicycling", + "Housework" + ] + }, + { + "name": "Evy", + "hobbies": [ + "Woodworking", + "Collecting" + ] + } + ] + }, + { + "id": 41, + "name": "Luke", + "city": "Nashville", + "age": 84, + "friends": [ + { + "name": "Lucas", + "hobbies": [ + "Fishing", + "Shopping" + ] + }, + { + "name": "Victoria", + "hobbies": [ + "Church Activities", + "Martial Arts", + "Television" + ] + }, + { + "name": "Charlotte", + "hobbies": [ + "Church Activities", + "Walking" + ] + }, + { + "name": "Evy", + "hobbies": [ + "Calligraphy", + "Writing" + ] + }, + { + "name": "Liam", + "hobbies": [ + "Movie Watching", + "Board Games" + ] + } + ] + }, + { + "id": 42, + "name": "Joe", + "city": "Orlando", + "age": 28, + "friends": [ + { + "name": "Sophie", + "hobbies": [ + "Board Games", + "Music" + ] + }, + { + "name": "Camila", + "hobbies": [ + "Woodworking", + "Yoga", + "Music" + ] + }, + { + "name": "Jack", + "hobbies": [ + "Team Sports", + "Bicycling" + ] + } + ] + }, + { + "id": 43, + "name": "Robert", + "city": "Boston", + "age": 89, + "friends": [ + { + "name": "Mia", + "hobbies": [ + "Team Sports", + "Church Activities", + "Martial Arts" + ] + }, + { + "name": "Elijah", + "hobbies": [ + "Housework", + "Collecting" + ] + }, + { + "name": "Zoey", + "hobbies": [ + "Watching Sports", + "Golf", + "Podcasts" + ] + }, + { + "name": "Sarah", + "hobbies": [ + "Volunteer Work", + "Team Sports" + ] + }, + { + "name": "Chloe", + "hobbies": [ + "Yoga", + "Walking" + ] + }, + { + "name": "Mateo", + "hobbies": [ + "Running", + "Painting", + "Television" + ] + } + ] + }, + { + "id": 44, + "name": "Mateo", + "city": "Palm Springs", + "age": 75, + "friends": [ + { + "name": "Sarah", + "hobbies": [ + "Socializing", + "Walking", + "Painting" + ] + }, + { + "name": "Sophie", + "hobbies": [ + "Skiing & Snowboarding", + "Bicycling", + "Eating Out" + ] + }, + { + "name": "Zoey", + "hobbies": [ + "Podcasts", + "Socializing", + "Calligraphy" + ] + }, + { + "name": "Olivia", + "hobbies": [ + "Dancing", + "Volunteer Work" + ] + }, + { + "name": "Mia", + "hobbies": [ + "Watching Sports", + "Yoga", + "Martial Arts" + ] + }, + { + "name": "Mateo", + "hobbies": [ + "Housework", + "Genealogy" + ] + } + ] + }, + { + "id": 45, + "name": "Michelle", + "city": "Portland", + "age": 64, + "friends": [ + { + "name": "Ava", + "hobbies": [ + "Watching Sports", + "Shopping" + ] + }, + { + "name": "John", + "hobbies": [ + "Martial Arts", + "Video Games", + "Fishing" + ] + } + ] + }, + { + "id": 46, + "name": "Emma", + "city": "Portland", + "age": 47, + "friends": [ + { + "name": "Zoey", + "hobbies": [ + "Yoga", + "Music", + "Bicycling" + ] + }, + { + "name": "Olivia", + "hobbies": [ + "Traveling", + "Movie Watching", + "Gardening" + ] + } + ] + }, + { + "id": 47, + "name": "Elijah", + "city": "Chicago", + "age": 96, + "friends": [ + { + "name": "Kevin", + "hobbies": [ + "Video Games", + "Watching Sports", + "Eating Out" + ] + }, + { + "name": "Kevin", + "hobbies": [ + "Housework", + "Tennis", + "Playing Cards" + ] + }, + { + "name": "Liam", + "hobbies": [ + "Cooking" + ] + }, + { + "name": "Kevin", + "hobbies": [ + "Genealogy", + "Housework" + ] + } + ] + }, + { + "id": 48, + "name": "Elijah", + "city": "Seattle", + "age": 30, + "friends": [ + { + "name": "Kevin", + "hobbies": [ + "Socializing", + "Eating Out" + ] + }, + { + "name": "Olivia", + "hobbies": [ + "Martial Arts", + "Golf", + "Cooking" + ] + }, + { + "name": "Daniel", + "hobbies": [ + "Gardening", + "Bicycling", + "Television" + ] + } + ] + }, + { + "id": 49, + "name": "Sophie", + "city": "Palm Springs", + "age": 84, + "friends": [ + { + "name": "Victoria", + "hobbies": [ + "Podcasts", + "Martial Arts" + ] + }, + { + "name": "Nora", + "hobbies": [ + "Volunteer Work", + "Bicycling", + "Reading" + ] + }, + { + "name": "Noah", + "hobbies": [ + "Television", + "Watching Sports", + "Traveling" + ] + }, + { + "name": "Victoria", + "hobbies": [ + "Bicycling", + "Woodworking", + "Tennis" + ] + } + ] + }, + { + "id": 50, + "name": "Sophie", + "city": "Chicago", + "age": 52, + "friends": [ + { + "name": "Chris", + "hobbies": [ + "Collecting", + "Dancing", + "Cooking" + ] + }, + { + "name": "Emma", + "hobbies": [ + "Watching Sports", + "Dancing", + "Tennis" + ] + }, + { + "name": "Joe", + "hobbies": [ + "Board Games", + "Skiing & Snowboarding" + ] + }, + { + "name": "Jack", + "hobbies": [ + "Calligraphy", + "Running" + ] + }, + { + "name": "John", + "hobbies": [ + "Quilting", + "Golf", + "Gardening" + ] + }, + { + "name": "John", + "hobbies": [ + "Watching Sports", + "Jewelry Making" + ] + } + ] + }, + { + "id": 51, + "name": "Nora", + "city": "Lahaina", + "age": 79, + "friends": [ + { + "name": "Elijah", + "hobbies": [ + "Skiing & Snowboarding", + "Martial Arts" + ] + }, + { + "name": "Kevin", + "hobbies": [ + "Volunteer Work", + "Running", + "Tennis" + ] + }, + { + "name": "Michael", + "hobbies": [ + "Skiing & Snowboarding", + "Quilting", + "Fishing" + ] + } + ] + }, + { + "id": 52, + "name": "Chris", + "city": "Miami Beach", + "age": 59, + "friends": [ + { + "name": "Amelia", + "hobbies": [ + "Video Games", + "Traveling", + "Cooking" + ] + }, + { + "name": "Kevin", + "hobbies": [ + "Shopping", + "Calligraphy" + ] + }, + { + "name": "Luke", + "hobbies": [ + "Playing Cards", + "Housework" + ] + }, + { + "name": "Chloe", + "hobbies": [ + "Painting", + "Housework", + "Shopping" + ] + } + ] + }, + { + "id": 53, + "name": "Kevin", + "city": "Boston", + "age": 88, + "friends": [ + { + "name": "Zoey", + "hobbies": [ + "Dancing" + ] + }, + { + "name": "Liam", + "hobbies": [ + "Traveling", + "Martial Arts" + ] + }, + { + "name": "Robert", + "hobbies": [ + "Woodworking", + "Collecting" + ] + }, + { + "name": "Elijah", + "hobbies": [ + "Collecting", + "Running" + ] + }, + { + "name": "Daniel", + "hobbies": [ + "Dancing", + "Traveling" + ] + }, + { + "name": "Emily", + "hobbies": [ + "Fishing", + "Quilting", + "Team Sports" + ] + } + ] + }, + { + "id": 54, + "name": "Grace", + "city": "Miami Beach", + "age": 62, + "friends": [ + { + "name": "Joe", + "hobbies": [ + "Church Activities", + "Music" + ] + }, + { + "name": "Emily", + "hobbies": [ + "Genealogy", + "Watching Sports", + "Woodworking" + ] + }, + { + "name": "Chris", + "hobbies": [ + "Team Sports", + "Skiing & Snowboarding" + ] + }, + { + "name": "Grace", + "hobbies": [ + "Yoga", + "Music", + "Running" + ] + } + ] + }, + { + "id": 55, + "name": "Chloe", + "city": "Lahaina", + "age": 97, + "friends": [ + { + "name": "Emily", + "hobbies": [ + "Genealogy", + "Team Sports", + "Skiing & Snowboarding" + ] + }, + { + "name": "Joe", + "hobbies": [ + "Movie Watching", + "Television" + ] + } + ] + }, + { + "id": 56, + "name": "Zoey", + "city": "Saint Augustine", + "age": 75, + "friends": [ + { + "name": "Luke", + "hobbies": [ + "Bicycling", + "Martial Arts" + ] + }, + { + "name": "Emma", + "hobbies": [ + "Music", + "Cooking" + ] + } + ] + }, + { + "id": 57, + "name": "Sophie", + "city": "Boston", + "age": 26, + "friends": [ + { + "name": "Levi", + "hobbies": [ + "Writing", + "Yoga", + "Movie Watching" + ] + }, + { + "name": "Noah", + "hobbies": [ + "Board Games", + "Martial Arts", + "Shopping" + ] + }, + { + "name": "Camila", + "hobbies": [ + "Jewelry Making", + "Skiing & Snowboarding", + "Fishing" + ] + } + ] + }, + { + "id": 58, + "name": "Emma", + "city": "Seattle", + "age": 40, + "friends": [ + { + "name": "Victoria", + "hobbies": [ + "Traveling", + "Bicycling" + ] + }, + { + "name": "Michelle", + "hobbies": [ + "Skiing & Snowboarding", + "Bicycling", + "Watching Sports" + ] + }, + { + "name": "Michael", + "hobbies": [ + "Board Games", + "Watching Sports" + ] + }, + { + "name": "Daniel", + "hobbies": [ + "Yoga", + "Shopping" + ] + } + ] + }, + { + "id": 59, + "name": "Luke", + "city": "San Diego", + "age": 44, + "friends": [ + { + "name": "Mia", + "hobbies": [ + "Calligraphy", + "Writing" + ] + }, + { + "name": "Michelle", + "hobbies": [ + "Podcasts", + "Movie Watching", + "Playing Cards" + ] + }, + { + "name": "Charlotte", + "hobbies": [ + "Bicycling", + "Golf", + "Walking" + ] + } + ] + }, + { + "id": 60, + "name": "Chloe", + "city": "Austin", + "age": 23, + "friends": [ + { + "name": "Camila", + "hobbies": [ + "Martial Arts", + "Golf" + ] + }, + { + "name": "Sarah", + "hobbies": [ + "Writing", + "Martial Arts", + "Jewelry Making" + ] + }, + { + "name": "Amelia", + "hobbies": [ + "Video Games", + "Bicycling", + "Eating Out" + ] + }, + { + "name": "Sophie", + "hobbies": [ + "Socializing", + "Collecting", + "Traveling" + ] + }, + { + "name": "Olivia", + "hobbies": [ + "Team Sports", + "Woodworking", + "Collecting" + ] + }, + { + "name": "Elijah", + "hobbies": [ + "Yoga", + "Music", + "Skiing & Snowboarding" + ] + } + ] + }, + { + "id": 61, + "name": "Nora", + "city": "Orlando", + "age": 83, + "friends": [ + { + "name": "Zoey", + "hobbies": [ + "Board Games", + "Woodworking" + ] + }, + { + "name": "Emma", + "hobbies": [ + "Board Games", + "Traveling" + ] + }, + { + "name": "Nora", + "hobbies": [ + "Bicycling", + "Golf" + ] + }, + { + "name": "Leo", + "hobbies": [ + "Church Activities", + "Golf", + "Socializing" + ] + }, + { + "name": "Oliver", + "hobbies": [ + "Running", + "Dancing" + ] + }, + { + "name": "Amelia", + "hobbies": [ + "Board Games", + "Volunteer Work" + ] + } + ] + }, + { + "id": 62, + "name": "Kevin", + "city": "Saint Augustine", + "age": 76, + "friends": [ + { + "name": "Lucas", + "hobbies": [ + "Playing Cards", + "Skiing & Snowboarding" + ] + }, + { + "name": "Grace", + "hobbies": [ + "Movie Watching", + "Calligraphy", + "Socializing" + ] + }, + { + "name": "Ava", + "hobbies": [ + "Podcasts", + "Yoga", + "Quilting" + ] + } + ] + }, + { + "id": 63, + "name": "Amelia", + "city": "Honolulu", + "age": 84, + "friends": [ + { + "name": "Grace", + "hobbies": [ + "Golf", + "Reading" + ] + }, + { + "name": "Luke", + "hobbies": [ + "Genealogy", + "Martial Arts" + ] + }, + { + "name": "Lucas", + "hobbies": [ + "Gardening", + "Music" + ] + }, + { + "name": "Isabella", + "hobbies": [ + "Board Games", + "Music" + ] + }, + { + "name": "Noah", + "hobbies": [ + "Cooking", + "Eating Out", + "Quilting" + ] + }, + { + "name": "Levi", + "hobbies": [ + "Movie Watching", + "Church Activities", + "Shopping" + ] + } + ] + }, + { + "id": 64, + "name": "Joe", + "city": "San Francisco", + "age": 37, + "friends": [ + { + "name": "Michelle", + "hobbies": [ + "Skiing & Snowboarding", + "Dancing" + ] + }, + { + "name": "John", + "hobbies": [ + "Running", + "Podcasts", + "Woodworking" + ] + } + ] + }, + { + "id": 65, + "name": "Chloe", + "city": "Palm Springs", + "age": 60, + "friends": [ + { + "name": "Lucas", + "hobbies": [ + "Movie Watching", + "Walking" + ] + }, + { + "name": "Grace", + "hobbies": [ + "Volunteer Work", + "Socializing" + ] + }, + { + "name": "Daniel", + "hobbies": [ + "Church Activities", + "Gardening" + ] + }, + { + "name": "Michael", + "hobbies": [ + "Walking", + "Team Sports", + "Martial Arts" + ] + } + ] + }, + { + "id": 66, + "name": "Leo", + "city": "New Orleans", + "age": 97, + "friends": [ + { + "name": "Ava", + "hobbies": [ + "Martial Arts", + "Woodworking", + "Quilting" + ] + }, + { + "name": "Chloe", + "hobbies": [ + "Fishing", + "Genealogy", + "Writing" + ] + }, + { + "name": "Nora", + "hobbies": [ + "Traveling", + "Woodworking" + ] + }, + { + "name": "Victoria", + "hobbies": [ + "Church Activities", + "Cooking" + ] + }, + { + "name": "Camila", + "hobbies": [ + "Video Games", + "Housework" + ] + } + ] + }, + { + "id": 67, + "name": "Robert", + "city": "Austin", + "age": 19, + "friends": [ + { + "name": "Joe", + "hobbies": [ + "Writing", + "Yoga" + ] + }, + { + "name": "Emma", + "hobbies": [ + "Writing", + "Socializing" + ] + } + ] + }, + { + "id": 68, + "name": "Robert", + "city": "Orlando", + "age": 65, + "friends": [ + { + "name": "Mateo", + "hobbies": [ + "Board Games", + "Cooking" + ] + }, + { + "name": "Noah", + "hobbies": [ + "Collecting", + "Housework", + "Skiing & Snowboarding" + ] + } + ] + }, + { + "id": 69, + "name": "Mateo", + "city": "New Orleans", + "age": 95, + "friends": [ + { + "name": "Chloe", + "hobbies": [ + "Painting", + "Eating Out", + "Walking" + ] + }, + { + "name": "Nora", + "hobbies": [ + "Bicycling", + "Jewelry Making", + "Woodworking" + ] + }, + { + "name": "Jack", + "hobbies": [ + "Cooking", + "Gardening" + ] + }, + { + "name": "Liam", + "hobbies": [ + "Reading", + "Collecting", + "Podcasts" + ] + }, + { + "name": "Noah", + "hobbies": [ + "Housework", + "Team Sports" + ] + }, + { + "name": "Emily", + "hobbies": [ + "Dancing", + "Yoga" + ] + } + ] + }, + { + "id": 70, + "name": "Jack", + "city": "Boston", + "age": 76, + "friends": [ + { + "name": "Ava", + "hobbies": [ + "Martial Arts", + "Volunteer Work", + "Team Sports" + ] + }, + { + "name": "Kevin", + "hobbies": [ + "Traveling", + "Bicycling" + ] + }, + { + "name": "Lucas", + "hobbies": [ + "Podcasts", + "Jewelry Making", + "Dancing" + ] + }, + { + "name": "Michelle", + "hobbies": [ + "Gardening", + "Shopping", + "Genealogy" + ] + }, + { + "name": "Michelle", + "hobbies": [ + "Writing", + "Tennis" + ] + } + ] + }, + { + "id": 71, + "name": "Liam", + "city": "Savannah", + "age": 37, + "friends": [ + { + "name": "Michelle", + "hobbies": [ + "Painting", + "Volunteer Work" + ] + }, + { + "name": "Kevin", + "hobbies": [ + "Dancing", + "Fishing" + ] + }, + { + "name": "Olivia", + "hobbies": [ + "Television", + "Running" + ] + }, + { + "name": "Robert", + "hobbies": [ + "Fishing", + "Eating Out" + ] + }, + { + "name": "John", + "hobbies": [ + "Skiing & Snowboarding" + ] + }, + { + "name": "Chloe", + "hobbies": [ + "Church Activities", + "Calligraphy", + "Writing" + ] + } + ] + }, + { + "id": 72, + "name": "Daniel", + "city": "Los Angeles", + "age": 63, + "friends": [ + { + "name": "Evy", + "hobbies": [ + "Television", + "Dancing" + ] + }, + { + "name": "Kevin", + "hobbies": [ + "Walking", + "Socializing", + "Writing" + ] + } + ] + }, + { + "id": 73, + "name": "Olivia", + "city": "Boston", + "age": 89, + "friends": [ + { + "name": "Ava", + "hobbies": [ + "Fishing", + "Playing Cards" + ] + }, + { + "name": "Noah", + "hobbies": [ + "Movie Watching", + "Board Games" + ] + } + ] + }, + { + "id": 74, + "name": "Amelia", + "city": "Orlando", + "age": 40, + "friends": [ + { + "name": "Kevin", + "hobbies": [ + "Golf", + "Reading", + "Shopping" + ] + }, + { + "name": "Liam", + "hobbies": [ + "Writing", + "Woodworking" + ] + }, + { + "name": "Amelia", + "hobbies": [ + "Movie Watching", + "Music" + ] + }, + { + "name": "Grace", + "hobbies": [ + "Jewelry Making", + "Bicycling" + ] + } + ] + }, + { + "id": 75, + "name": "Camila", + "city": "New Orleans", + "age": 65, + "friends": [ + { + "name": "Mateo", + "hobbies": [ + "Yoga", + "Reading", + "Team Sports" + ] + }, + { + "name": "Mateo", + "hobbies": [ + "Board Games", + "Shopping" + ] + }, + { + "name": "Leo", + "hobbies": [ + "Woodworking", + "Martial Arts" + ] + }, + { + "name": "Jack", + "hobbies": [ + "Television", + "Calligraphy", + "Playing Cards" + ] + }, + { + "name": "Liam", + "hobbies": [ + "Fishing", + "Martial Arts" + ] + } + ] + }, + { + "id": 76, + "name": "Jack", + "city": "Orlando", + "age": 42, + "friends": [ + { + "name": "Daniel", + "hobbies": [ + "Podcasts", + "Collecting" + ] + }, + { + "name": "Robert", + "hobbies": [ + "Running", + "Shopping", + "Quilting" + ] + }, + { + "name": "Chris", + "hobbies": [ + "Martial Arts", + "Golf", + "Quilting" + ] + }, + { + "name": "Amelia", + "hobbies": [ + "Eating Out", + "Bicycling", + "Golf" + ] + }, + { + "name": "Olivia", + "hobbies": [ + "Skiing & Snowboarding", + "Church Activities" + ] + } + ] + }, + { + "id": 77, + "name": "Leo", + "city": "Lahaina", + "age": 46, + "friends": [ + { + "name": "Robert", + "hobbies": [ + "Traveling", + "Watching Sports" + ] + }, + { + "name": "Daniel", + "hobbies": [ + "Video Games", + "Music" + ] + }, + { + "name": "Michael", + "hobbies": [ + "Video Games", + "Gardening" + ] + }, + { + "name": "Emma", + "hobbies": [ + "Painting", + "Television" + ] + }, + { + "name": "Sarah", + "hobbies": [ + "Dancing", + "Tennis" + ] + } + ] + }, + { + "id": 78, + "name": "Kevin", + "city": "San Antonio", + "age": 19, + "friends": [ + { + "name": "Leo", + "hobbies": [ + "Traveling", + "Television" + ] + }, + { + "name": "Victoria", + "hobbies": [ + "Fishing", + "Collecting", + "Gardening" + ] + }, + { + "name": "Mia", + "hobbies": [ + "Skiing & Snowboarding", + "Watching Sports", + "Martial Arts" + ] + } + ] + }, + { + "id": 79, + "name": "Leo", + "city": "Sedona", + "age": 56, + "friends": [ + { + "name": "Mateo", + "hobbies": [ + "Board Games", + "Reading" + ] + }, + { + "name": "Grace", + "hobbies": [ + "Reading", + "Fishing", + "Woodworking" + ] + }, + { + "name": "Mia", + "hobbies": [ + "Gardening", + "Woodworking" + ] + }, + { + "name": "Noah", + "hobbies": [ + "Video Games", + "Television", + "Eating Out" + ] + } + ] + }, + { + "id": 80, + "name": "Charlotte", + "city": "Orlando", + "age": 73, + "friends": [ + { + "name": "Camila", + "hobbies": [ + "Golf", + "Collecting" + ] + }, + { + "name": "Evy", + "hobbies": [ + "Shopping", + "Yoga", + "Genealogy" + ] + }, + { + "name": "Charlotte", + "hobbies": [ + "Yoga", + "Volunteer Work" + ] + } + ] + }, + { + "id": 81, + "name": "Robert", + "city": "Chicago", + "age": 52, + "friends": [ + { + "name": "Noah", + "hobbies": [ + "Church Activities", + "Woodworking", + "Traveling" + ] + }, + { + "name": "Liam", + "hobbies": [ + "Board Games", + "Socializing" + ] + }, + { + "name": "Liam", + "hobbies": [ + "Housework", + "Music", + "Calligraphy" + ] + }, + { + "name": "Zoey", + "hobbies": [ + "Shopping", + "Fishing", + "Walking" + ] + }, + { + "name": "Grace", + "hobbies": [ + "Dancing", + "Yoga" + ] + } + ] + }, + { + "id": 82, + "name": "Kevin", + "city": "Palm Springs", + "age": 75, + "friends": [ + { + "name": "Oliver", + "hobbies": [ + "Running", + "Traveling" + ] + }, + { + "name": "Luke", + "hobbies": [ + "Socializing", + "Martial Arts", + "Running" + ] + } + ] + }, + { + "id": 83, + "name": "Evy", + "city": "Palm Springs", + "age": 51, + "friends": [ + { + "name": "Michael", + "hobbies": [ + "Writing", + "Podcasts" + ] + }, + { + "name": "Noah", + "hobbies": [ + "Yoga", + "Quilting", + "Fishing" + ] + }, + { + "name": "Sophie", + "hobbies": [ + "Painting" + ] + }, + { + "name": "Olivia", + "hobbies": [ + "Martial Arts", + "Shopping", + "Podcasts" + ] + }, + { + "name": "Camila", + "hobbies": [ + "Reading", + "Collecting" + ] + }, + { + "name": "Noah", + "hobbies": [ + "Socializing", + "Housework" + ] + } + ] + }, + { + "id": 84, + "name": "Daniel", + "city": "Saint Augustine", + "age": 57, + "friends": [ + { + "name": "Emily", + "hobbies": [ + "Walking", + "Painting", + "Reading" + ] + }, + { + "name": "Oliver", + "hobbies": [ + "Team Sports", + "Board Games" + ] + }, + { + "name": "Levi", + "hobbies": [ + "Jewelry Making", + "Eating Out", + "Volunteer Work" + ] + }, + { + "name": "Zoey", + "hobbies": [ + "Movie Watching", + "Video Games" + ] + }, + { + "name": "Sophie", + "hobbies": [ + "Watching Sports", + "Walking", + "Martial Arts" + ] + } + ] + }, + { + "id": 85, + "name": "Olivia", + "city": "Charleston", + "age": 63, + "friends": [ + { + "name": "Oliver", + "hobbies": [ + "Reading", + "Playing Cards" + ] + }, + { + "name": "Mia", + "hobbies": [ + "Running", + "Shopping" + ] + }, + { + "name": "John", + "hobbies": [ + "Writing", + "Walking", + "Tennis" + ] + } + ] + }, + { + "id": 86, + "name": "Amelia", + "city": "Seattle", + "age": 96, + "friends": [ + { + "name": "Daniel", + "hobbies": [ + "Dancing", + "Eating Out" + ] + }, + { + "name": "Levi", + "hobbies": [ + "Bicycling", + "Dancing" + ] + }, + { + "name": "Daniel", + "hobbies": [ + "Writing", + "Shopping", + "Tennis" + ] + }, + { + "name": "Michelle", + "hobbies": [ + "Board Games", + "Walking", + "Housework" + ] + }, + { + "name": "Chloe", + "hobbies": [ + "Genealogy", + "Dancing", + "Podcasts" + ] + }, + { + "name": "Joe", + "hobbies": [ + "Movie Watching", + "Cooking", + "Housework" + ] + } + ] + }, + { + "id": 87, + "name": "Luke", + "city": "Seattle", + "age": 26, + "friends": [ + { + "name": "Isabella", + "hobbies": [ + "Traveling", + "Walking", + "Team Sports" + ] + }, + { + "name": "Amelia", + "hobbies": [ + "Writing", + "Housework", + "Volunteer Work" + ] + }, + { + "name": "Elijah", + "hobbies": [ + "Golf", + "Yoga" + ] + }, + { + "name": "Levi", + "hobbies": [ + "Volunteer Work", + "Eating Out" + ] + }, + { + "name": "Kevin", + "hobbies": [ + "Yoga", + "Genealogy", + "Volunteer Work" + ] + }, + { + "name": "Levi", + "hobbies": [ + "Tennis", + "Television" + ] + } + ] + }, + { + "id": 88, + "name": "Chris", + "city": "Nashville", + "age": 34, + "friends": [ + { + "name": "Kevin", + "hobbies": [ + "Podcasts", + "Team Sports", + "Traveling" + ] + }, + { + "name": "Elijah", + "hobbies": [ + "Television", + "Woodworking", + "Cooking" + ] + }, + { + "name": "Evy", + "hobbies": [ + "Podcasts", + "Genealogy", + "Calligraphy" + ] + }, + { + "name": "Victoria", + "hobbies": [ + "Fishing", + "Church Activities", + "Collecting" + ] + }, + { + "name": "Camila", + "hobbies": [ + "Television", + "Writing" + ] + }, + { + "name": "Michelle", + "hobbies": [ + "Yoga", + "Running" + ] + } + ] + }, + { + "id": 89, + "name": "Michelle", + "city": "Honolulu", + "age": 85, + "friends": [ + { + "name": "Isabella", + "hobbies": [ + "Calligraphy", + "Gardening" + ] + }, + { + "name": "Chloe", + "hobbies": [ + "Shopping", + "Playing Cards", + "Tennis" + ] + }, + { + "name": "Evy", + "hobbies": [ + "Watching Sports", + "Cooking", + "Golf" + ] + }, + { + "name": "Elijah", + "hobbies": [ + "Writing", + "Tennis", + "Playing Cards" + ] + } + ] + }, + { + "id": 90, + "name": "Lucas", + "city": "Los Angeles", + "age": 78, + "friends": [ + { + "name": "Emma", + "hobbies": [ + "Woodworking", + "Painting", + "Television" + ] + }, + { + "name": "Lucas", + "hobbies": [ + "Bicycling", + "Volunteer Work" + ] + }, + { + "name": "Grace", + "hobbies": [ + "Dancing", + "Running" + ] + } + ] + }, + { + "id": 91, + "name": "Sophie", + "city": "St. Louis", + "age": 86, + "friends": [ + { + "name": "Joe", + "hobbies": [ + "Socializing", + "Music" + ] + }, + { + "name": "Zoey", + "hobbies": [ + "Running", + "Playing Cards" + ] + }, + { + "name": "Elijah", + "hobbies": [ + "Dancing" + ] + } + ] + }, + { + "id": 92, + "name": "Victoria", + "city": "Saint Augustine", + "age": 33, + "friends": [ + { + "name": "Leo", + "hobbies": [ + "Socializing", + "Fishing" + ] + }, + { + "name": "Emily", + "hobbies": [ + "Video Games", + "Watching Sports" + ] + }, + { + "name": "Luke", + "hobbies": [ + "Martial Arts" + ] + }, + { + "name": "Oliver", + "hobbies": [ + "Traveling", + "Quilting", + "Television" + ] + }, + { + "name": "Charlotte", + "hobbies": [ + "Gardening", + "Cooking", + "Housework" + ] + } + ] + }, + { + "id": 93, + "name": "Michael", + "city": "New Orleans", + "age": 82, + "friends": [ + { + "name": "Jack", + "hobbies": [ + "Bicycling", + "Board Games", + "Movie Watching" + ] + }, + { + "name": "Liam", + "hobbies": [ + "Painting", + "Writing", + "Bicycling" + ] + } + ] + }, + { + "id": 94, + "name": "Michael", + "city": "Seattle", + "age": 49, + "friends": [ + { + "name": "John", + "hobbies": [ + "Collecting", + "Playing Cards", + "Cooking" + ] + }, + { + "name": "Sarah", + "hobbies": [ + "Fishing", + "Walking", + "Movie Watching" + ] + } + ] + }, + { + "id": 95, + "name": "Victoria", + "city": "Branson", + "age": 48, + "friends": [ + { + "name": "Amelia", + "hobbies": [ + "Painting", + "Volunteer Work", + "Socializing" + ] + }, + { + "name": "Evy", + "hobbies": [ + "Skiing & Snowboarding", + "Volunteer Work" + ] + }, + { + "name": "Amelia", + "hobbies": [ + "Genealogy", + "Reading", + "Yoga" + ] + }, + { + "name": "Sophie", + "hobbies": [ + "Movie Watching", + "Golf", + "Television" + ] + }, + { + "name": "Charlotte", + "hobbies": [ + "Jewelry Making", + "Quilting", + "Playing Cards" + ] + }, + { + "name": "Jack", + "hobbies": [ + "Playing Cards", + "Golf" + ] + } + ] + }, + { + "id": 96, + "name": "Grace", + "city": "Seattle", + "age": 89, + "friends": [ + { + "name": "Chris", + "hobbies": [ + "Board Games", + "Golf", + "Playing Cards" + ] + }, + { + "name": "Emily", + "hobbies": [ + "Video Games", + "Golf" + ] + }, + { + "name": "Victoria", + "hobbies": [ + "Housework", + "Collecting", + "Woodworking" + ] + } + ] + }, + { + "id": 97, + "name": "Liam", + "city": "Nashville", + "age": 64, + "friends": [ + { + "name": "Kevin", + "hobbies": [ + "Collecting" + ] + }, + { + "name": "Amelia", + "hobbies": [ + "Golf", + "Playing Cards", + "Cooking" + ] + }, + { + "name": "Charlotte", + "hobbies": [ + "Reading", + "Board Games", + "Genealogy" + ] + }, + { + "name": "Leo", + "hobbies": [ + "Video Games", + "Writing" + ] + }, + { + "name": "Nora", + "hobbies": [ + "Jewelry Making", + "Volunteer Work" + ] + } + ] + }, + { + "id": 98, + "name": "Mia", + "city": "Miami Beach", + "age": 77, + "friends": [ + { + "name": "Emma", + "hobbies": [ + "Podcasts", + "Movie Watching" + ] + }, + { + "name": "Oliver", + "hobbies": [ + "Playing Cards", + "Fishing", + "Eating Out" + ] + }, + { + "name": "Emma", + "hobbies": [ + "Collecting", + "Yoga" + ] + }, + { + "name": "Michael", + "hobbies": [ + "Bicycling", + "Team Sports" + ] + }, + { + "name": "Ava", + "hobbies": [ + "Watching Sports", + "Jewelry Making" + ] + }, + { + "name": "Joe", + "hobbies": [ + "Video Games", + "Woodworking", + "Music" + ] + } + ] + }, + { + "id": 99, + "name": "Mateo", + "city": "Branson", + "age": 66, + "friends": [ + { + "name": "Isabella", + "hobbies": [ + "Television", + "Skiing & Snowboarding" + ] + }, + { + "name": "Noah", + "hobbies": [ + "Housework", + "Running", + "Podcasts" + ] + } + ] + } + ] +} \ No newline at end of file From d3c7eaf17e8ca0162d344c3e98ef270b8164cc69 Mon Sep 17 00:00:00 2001 From: Sean Leary Date: Sun, 15 Dec 2024 13:18:39 -0600 Subject: [PATCH 5/6] restore-jsonparserconfiguration: fix unit tests to work when strictMode default is true --- src/test/java/org/json/junit/CDLTest.java | 59 +- .../java/org/json/junit/JSONArrayTest.java | 21 +- src/test/java/org/json/junit/JSONMLTest.java | 2 +- .../org/json/junit/JSONObjectNumberTest.java | 24 +- .../java/org/json/junit/JSONObjectTest.java | 739 +++++++++--------- .../java/org/json/junit/JSONPointerTest.java | 3 +- .../org/json/junit/XMLConfigurationTest.java | 4 +- src/test/java/org/json/junit/XMLTest.java | 8 +- 8 files changed, 461 insertions(+), 399 deletions(-) diff --git a/src/test/java/org/json/junit/CDLTest.java b/src/test/java/org/json/junit/CDLTest.java index cc3da29..0e3668b 100644 --- a/src/test/java/org/json/junit/CDLTest.java +++ b/src/test/java/org/json/junit/CDLTest.java @@ -29,7 +29,7 @@ public class CDLTest { "1, 2, 3, 4\t, 5, 6, 7\n" + "true, false, true, true, false, false, false\n" + "0.23, 57.42, 5e27, -234.879, 2.34e5, 0.0, 9e-3\n" + - "\"va\tl1\", \"v\bal2\", \"val3\", \"val\f4\", \"val5\", va'l6, val7\n"; + "\"va\tl1\", \"v\bal2\", \"val3\", \"val\f4\", \"val5\", \"va'l6\", val7\n"; /** @@ -38,11 +38,54 @@ public class CDLTest { * values all must be quoted in the cases where the JSONObject parsing * might normally convert the value into a non-string. */ - private static final String EXPECTED_LINES = "[{Col 1:val1, Col 2:val2, Col 3:val3, Col 4:val4, Col 5:val5, Col 6:val6, Col 7:val7}, " + - "{Col 1:\"1\", Col 2:\"2\", Col 3:\"3\", Col 4:\"4\", Col 5:\"5\", Col 6:\"6\", Col 7:\"7\"}, " + - "{Col 1:\"true\", Col 2:\"false\", Col 3:\"true\", Col 4:\"true\", Col 5:\"false\", Col 6:\"false\", Col 7:\"false\"}, " + - "{Col 1:\"0.23\", Col 2:\"57.42\", Col 3:\"5e27\", Col 4:\"-234.879\", Col 5:\"2.34e5\", Col 6:\"0.0\", Col 7:\"9e-3\"}, " + - "{Col 1:\"va\tl1\", Col 2:\"v\bal2\", Col 3:val3, Col 4:\"val\f4\", Col 5:val5, Col 6:va'l6, Col 7:val7}]"; + private static final String EXPECTED_LINES = + "[ " + + "{" + + "\"Col 1\":\"val1\", " + + "\"Col 2\":\"val2\", " + + "\"Col 3\":\"val3\", " + + "\"Col 4\":\"val4\", " + + "\"Col 5\":\"val5\", " + + "\"Col 6\":\"val6\", " + + "\"Col 7\":\"val7\"" + + "}, " + + " {" + + "\"Col 1\":\"1\", " + + "\"Col 2\":\"2\", " + + "\"Col 3\":\"3\", " + + "\"Col 4\":\"4\", " + + "\"Col 5\":\"5\", " + + "\"Col 6\":\"6\", " + + "\"Col 7\":\"7\"" + + "}, " + + " {" + + "\"Col 1\":\"true\", " + + "\"Col 2\":\"false\", " + + "\"Col 3\":\"true\", " + + "\"Col 4\":\"true\", " + + "\"Col 5\":\"false\", " + + "\"Col 6\":\"false\", " + + "\"Col 7\":\"false\"" + + "}, " + + "{" + + "\"Col 1\":\"0.23\", " + + "\"Col 2\":\"57.42\", " + + "\"Col 3\":\"5e27\", " + + "\"Col 4\":\"-234.879\", " + + "\"Col 5\":\"2.34e5\", " + + "\"Col 6\":\"0.0\", " + + "\"Col 7\":\"9e-3\"" + + "}, " + + "{" + + "\"Col 1\":\"va\tl1\", " + + "\"Col 2\":\"v\bal2\", " + + "\"Col 3\":\"val3\", " + + "\"Col 4\":\"val\f4\", " + + "\"Col 5\":\"val5\", " + + "\"Col 6\":\"va'l6\", " + + "\"Col 7\":\"val7\"" + + "}" + + "]"; /** * Attempts to create a JSONArray from a null string. @@ -283,11 +326,11 @@ public class CDLTest { */ @Test public void jsonArrayToJSONArray() { - String nameArrayStr = "[Col1, Col2]"; + String nameArrayStr = "[\"Col1\", \"Col2\"]"; String values = "V1, V2"; JSONArray nameJSONArray = new JSONArray(nameArrayStr); JSONArray jsonArray = CDL.toJSONArray(nameJSONArray, values); - JSONArray expectedJsonArray = new JSONArray("[{Col1:V1,Col2:V2}]"); + JSONArray expectedJsonArray = new JSONArray("[{\"Col1\":\"V1\",\"Col2\":\"V2\"}]"); Util.compareActualVsExpectedJsonArrays(jsonArray, expectedJsonArray); } diff --git a/src/test/java/org/json/junit/JSONArrayTest.java b/src/test/java/org/json/junit/JSONArrayTest.java index 283bc97..ed7c9ba 100644 --- a/src/test/java/org/json/junit/JSONArrayTest.java +++ b/src/test/java/org/json/junit/JSONArrayTest.java @@ -476,10 +476,15 @@ public class JSONArrayTest { */ @Test public void unquotedText() { - String str = "[value1, something!, (parens), foo@bar.com, 23, 23+45]"; - JSONArray jsonArray = new JSONArray(str); - List expected = Arrays.asList("value1", "something!", "(parens)", "foo@bar.com", 23, "23+45"); - assertEquals(expected, jsonArray.toList()); + JSONParserConfiguration jsonParserConfiguration = new JSONParserConfiguration(); + if (jsonParserConfiguration.isStrictMode()) { + System.out.println("Skipping JSONArrayTest unquotedText() when strictMode default is true"); + } else { + String str = "[value1, something!, (parens), foo@bar.com, 23, 23+45]"; + JSONArray jsonArray = new JSONArray(str); + List expected = Arrays.asList("value1", "something!", "(parens)", "foo@bar.com", 23, "23+45"); + assertEquals(expected, jsonArray.toList()); + } } /** @@ -690,8 +695,8 @@ public class JSONArrayTest { String jsonArrayStr = "["+ - "hello,"+ - "world"+ + "\"hello\","+ + "\"world\""+ "]"; // 2 jsonArray.put(new JSONArray(jsonArrayStr)); @@ -768,8 +773,8 @@ public class JSONArrayTest { String jsonArrayStr = "["+ - "hello,"+ - "world"+ + "\"hello\","+ + "\"world\""+ "]"; // 2 jsonArray.put(2, new JSONArray(jsonArrayStr)); diff --git a/src/test/java/org/json/junit/JSONMLTest.java b/src/test/java/org/json/junit/JSONMLTest.java index 154af64..5a360dd 100644 --- a/src/test/java/org/json/junit/JSONMLTest.java +++ b/src/test/java/org/json/junit/JSONMLTest.java @@ -625,7 +625,7 @@ public class JSONMLTest { "\"subValue\","+ "{\"svAttr\":\"svValue\"},"+ "\"abc\""+ - "],"+ + "]"+ "],"+ "[\"value\",3],"+ "[\"value\",4.1],"+ diff --git a/src/test/java/org/json/junit/JSONObjectNumberTest.java b/src/test/java/org/json/junit/JSONObjectNumberTest.java index 43173a2..0f2af29 100644 --- a/src/test/java/org/json/junit/JSONObjectNumberTest.java +++ b/src/test/java/org/json/junit/JSONObjectNumberTest.java @@ -23,18 +23,18 @@ public class JSONObjectNumberTest { @Parameters(name = "{index}: {0}") public static Collection data() { return Arrays.asList(new Object[][]{ - {"{value:50}", 1}, - {"{value:50.0}", 1}, - {"{value:5e1}", 1}, - {"{value:5E1}", 1}, - {"{value:5e1}", 1}, - {"{value:'50'}", 1}, - {"{value:-50}", -1}, - {"{value:-50.0}", -1}, - {"{value:-5e1}", -1}, - {"{value:-5E1}", -1}, - {"{value:-5e1}", -1}, - {"{value:'-50'}", -1} + {"{\"value\":50}", 1}, + {"{\"value\":50.0}", 1}, + {"{\"value\":5e1}", 1}, + {"{\"value\":5E1}", 1}, + {"{\"value\":5e1}", 1}, + {"{\"value\":\"50\"}", 1}, + {"{\"value\":-50}", -1}, + {"{\"value\":-50.0}", -1}, + {"{\"value\":-5e1}", -1}, + {"{\"value\":-5E1}", -1}, + {"{\"value\":-5e1}", -1}, + {"{\"value\":\"-50\"}", -1} // JSON does not support octal or hex numbers; // see https://stackoverflow.com/a/52671839/6323312 // "{value:062}", // octal 50 diff --git a/src/test/java/org/json/junit/JSONObjectTest.java b/src/test/java/org/json/junit/JSONObjectTest.java index fbad313..ad4974b 100644 --- a/src/test/java/org/json/junit/JSONObjectTest.java +++ b/src/test/java/org/json/junit/JSONObjectTest.java @@ -218,18 +218,23 @@ public class JSONObjectTest { */ @Test public void unquotedText() { - String str = "{key1:value1, key2:42, 1.2 : 3.4, -7e5 : something!}"; - JSONObject jsonObject = new JSONObject(str); - String textStr = jsonObject.toString(); - assertTrue("expected key1", textStr.contains("\"key1\"")); - assertTrue("expected value1", textStr.contains("\"value1\"")); - assertTrue("expected key2", textStr.contains("\"key2\"")); - assertTrue("expected 42", textStr.contains("42")); - assertTrue("expected 1.2", textStr.contains("\"1.2\"")); - assertTrue("expected 3.4", textStr.contains("3.4")); - assertTrue("expected -7E+5", textStr.contains("\"-7E+5\"")); - assertTrue("expected something!", textStr.contains("\"something!\"")); - Util.checkJSONObjectMaps(jsonObject); + JSONParserConfiguration jsonParserConfiguration = new JSONParserConfiguration(); + if (jsonParserConfiguration.isStrictMode()) { + System.out.println("Skipping JSONObjectTest unquotedText() when strictMode default is true"); + } else { + String str = "{key1:value1, key2:42, 1.2 : 3.4, -7e5 : something!}"; + JSONObject jsonObject = new JSONObject(str); + String textStr = jsonObject.toString(); + assertTrue("expected key1", textStr.contains("\"key1\"")); + assertTrue("expected value1", textStr.contains("\"value1\"")); + assertTrue("expected key2", textStr.contains("\"key2\"")); + assertTrue("expected 42", textStr.contains("42")); + assertTrue("expected 1.2", textStr.contains("\"1.2\"")); + assertTrue("expected 3.4", textStr.contains("3.4")); + assertTrue("expected -7E+5", textStr.contains("\"-7E+5\"")); + assertTrue("expected something!", textStr.contains("\"something!\"")); + Util.checkJSONObjectMaps(jsonObject); + } } @Test @@ -1069,48 +1074,53 @@ public class JSONObjectTest { */ @Test public void jsonInvalidNumberValues() { + JSONParserConfiguration jsonParserConfiguration = new JSONParserConfiguration(); + if (jsonParserConfiguration.isStrictMode()) { + System.out.println("Skipping JSONObjectTest jsonInvalidNumberValues() when strictMode default is true"); + } else { // Number-notations supported by Java and invalid as JSON - String str = - "{"+ - "\"hexNumber\":-0x123,"+ - "\"tooManyZeros\":00,"+ - "\"negativeInfinite\":-Infinity,"+ - "\"negativeNaN\":-NaN,"+ - "\"negativeFraction\":-.01,"+ - "\"tooManyZerosFraction\":00.001,"+ - "\"negativeHexFloat\":-0x1.fffp1,"+ - "\"hexFloat\":0x1.0P-1074,"+ - "\"floatIdentifier\":0.1f,"+ - "\"doubleIdentifier\":0.1d"+ - "}"; - JSONObject jsonObject = new JSONObject(str); - Object obj; - obj = jsonObject.get( "hexNumber" ); - assertFalse( "hexNumber must not be a number (should throw exception!?)", - obj instanceof Number ); - assertTrue("hexNumber currently evaluates to string", - obj.equals("-0x123")); - assertTrue( "tooManyZeros currently evaluates to string", - jsonObject.get( "tooManyZeros" ).equals("00")); - obj = jsonObject.get("negativeInfinite"); - assertTrue( "negativeInfinite currently evaluates to string", - obj.equals("-Infinity")); - obj = jsonObject.get("negativeNaN"); - assertTrue( "negativeNaN currently evaluates to string", - obj.equals("-NaN")); - assertTrue( "negativeFraction currently evaluates to double -0.01", - jsonObject.get( "negativeFraction" ).equals(BigDecimal.valueOf(-0.01))); - assertTrue( "tooManyZerosFraction currently evaluates to double 0.001", - jsonObject.optLong( "tooManyZerosFraction" )==0); - assertTrue( "negativeHexFloat currently evaluates to double -3.99951171875", - jsonObject.get( "negativeHexFloat" ).equals(Double.valueOf(-3.99951171875))); - assertTrue("hexFloat currently evaluates to double 4.9E-324", - jsonObject.get("hexFloat").equals(Double.valueOf(4.9E-324))); - assertTrue("floatIdentifier currently evaluates to double 0.1", - jsonObject.get("floatIdentifier").equals(Double.valueOf(0.1))); - assertTrue("doubleIdentifier currently evaluates to double 0.1", - jsonObject.get("doubleIdentifier").equals(Double.valueOf(0.1))); - Util.checkJSONObjectMaps(jsonObject); + String str = + "{" + + "\"hexNumber\":-0x123," + + "\"tooManyZeros\":00," + + "\"negativeInfinite\":-Infinity," + + "\"negativeNaN\":-NaN," + + "\"negativeFraction\":-.01," + + "\"tooManyZerosFraction\":00.001," + + "\"negativeHexFloat\":-0x1.fffp1," + + "\"hexFloat\":0x1.0P-1074," + + "\"floatIdentifier\":0.1f," + + "\"doubleIdentifier\":0.1d" + + "}"; + JSONObject jsonObject = new JSONObject(str); + Object obj; + obj = jsonObject.get("hexNumber"); + assertFalse("hexNumber must not be a number (should throw exception!?)", + obj instanceof Number); + assertTrue("hexNumber currently evaluates to string", + obj.equals("-0x123")); + assertTrue("tooManyZeros currently evaluates to string", + jsonObject.get("tooManyZeros").equals("00")); + obj = jsonObject.get("negativeInfinite"); + assertTrue("negativeInfinite currently evaluates to string", + obj.equals("-Infinity")); + obj = jsonObject.get("negativeNaN"); + assertTrue("negativeNaN currently evaluates to string", + obj.equals("-NaN")); + assertTrue("negativeFraction currently evaluates to double -0.01", + jsonObject.get("negativeFraction").equals(BigDecimal.valueOf(-0.01))); + assertTrue("tooManyZerosFraction currently evaluates to double 0.001", + jsonObject.optLong("tooManyZerosFraction") == 0); + assertTrue("negativeHexFloat currently evaluates to double -3.99951171875", + jsonObject.get("negativeHexFloat").equals(Double.valueOf(-3.99951171875))); + assertTrue("hexFloat currently evaluates to double 4.9E-324", + jsonObject.get("hexFloat").equals(Double.valueOf(4.9E-324))); + assertTrue("floatIdentifier currently evaluates to double 0.1", + jsonObject.get("floatIdentifier").equals(Double.valueOf(0.1))); + assertTrue("doubleIdentifier currently evaluates to double 0.1", + jsonObject.get("doubleIdentifier").equals(Double.valueOf(0.1))); + Util.checkJSONObjectMaps(jsonObject); + } } /** @@ -1528,7 +1538,7 @@ public class JSONObjectTest { "{"+ "\"trueKey\":true,"+ "\"falseKey\":false,"+ - "\"stringKey\":\"hello world!\","+ + "\"stringKey\":\"hello world!\""+ "}"; JSONObject jsonObject2 = new JSONObject(str); names = JSONObject.getNames(jsonObject2); @@ -1623,7 +1633,7 @@ public class JSONObjectTest { "{"+ "\"trueKey\":true,"+ "\"falseKey\":false,"+ - "\"stringKey\":\"hello world!\","+ + "\"stringKey\":\"hello world!\""+ "}"; JSONObject jsonObject = new JSONObject(str); @@ -2262,321 +2272,326 @@ public class JSONObjectTest { @SuppressWarnings({"boxing", "unused"}) @Test public void jsonObjectParsingErrors() { - try { - // does not start with '{' - String str = "abc"; - assertNull("Expected an exception",new JSONObject(str)); - } catch (JSONException e) { - assertEquals("Expecting an exception message", - "A JSONObject text must begin with '{' at 1 [character 2 line 1]", - e.getMessage()); - } - try { - // does not end with '}' - String str = "{"; - assertNull("Expected an exception",new JSONObject(str)); - } catch (JSONException e) { - assertEquals("Expecting an exception message", - "A JSONObject text must end with '}' at 1 [character 2 line 1]", - e.getMessage()); - } - try { - // key with no ':' - String str = "{\"myKey\" = true}"; - assertNull("Expected an exception",new JSONObject(str)); - } catch (JSONException e) { - assertEquals("Expecting an exception message", - "Expected a ':' after a key at 10 [character 11 line 1]", - e.getMessage()); - } - try { - // entries with no ',' separator - String str = "{\"myKey\":true \"myOtherKey\":false}"; - assertNull("Expected an exception",new JSONObject(str)); - } catch (JSONException e) { - assertEquals("Expecting an exception message", - "Expected a ',' or '}' at 15 [character 16 line 1]", - e.getMessage()); - } - try { - // key is a nested map - String str = "{{\"foo\": \"bar\"}: \"baz\"}"; - assertNull("Expected an exception",new JSONObject(str)); - } catch (JSONException e) { - assertEquals("Expecting an exception message", - "Missing value at 1 [character 2 line 1]", - e.getMessage()); - } - try { - // key is a nested array containing a map - String str = "{\"a\": 1, [{\"foo\": \"bar\"}]: \"baz\"}"; - assertNull("Expected an exception",new JSONObject(str)); - } catch (JSONException e) { - assertEquals("Expecting an exception message", - "Missing value at 9 [character 10 line 1]", - e.getMessage()); - } - try { - // key contains } - String str = "{foo}: 2}"; - assertNull("Expected an exception",new JSONObject(str)); - } catch (JSONException e) { - assertEquals("Expecting an exception message", - "Expected a ':' after a key at 5 [character 6 line 1]", - e.getMessage()); - } - try { - // key contains ] - String str = "{foo]: 2}"; - assertNull("Expected an exception",new JSONObject(str)); - } catch (JSONException e) { - assertEquals("Expecting an exception message", - "Expected a ':' after a key at 5 [character 6 line 1]", - e.getMessage()); - } - try { - // \0 after , - String str = "{\"myKey\":true, \0\"myOtherKey\":false}"; - assertNull("Expected an exception",new JSONObject(str)); - } catch (JSONException e) { - assertEquals("Expecting an exception message", - "A JSONObject text must end with '}' at 15 [character 16 line 1]", - e.getMessage()); - } - try { - // append to wrong key - String str = "{\"myKey\":true, \"myOtherKey\":false}"; - JSONObject jsonObject = new JSONObject(str); - jsonObject.append("myKey", "hello"); - fail("Expected an exception"); - } catch (JSONException e) { - assertEquals("Expecting an exception message", - "JSONObject[\"myKey\"] is not a JSONArray (null).", - e.getMessage()); - } - try { - // increment wrong key - String str = "{\"myKey\":true, \"myOtherKey\":false}"; - JSONObject jsonObject = new JSONObject(str); - jsonObject.increment("myKey"); - fail("Expected an exception"); - } catch (JSONException e) { - assertEquals("Expecting an exception message", - "Unable to increment [\"myKey\"].", - e.getMessage()); - } - try { - // invalid key - String str = "{\"myKey\":true, \"myOtherKey\":false}"; - JSONObject jsonObject = new JSONObject(str); - jsonObject.get(null); - fail("Expected an exception"); - } catch (JSONException e) { - assertEquals("Expecting an exception message", - "Null key.", - e.getMessage()); - } - try { - // invalid numberToString() - JSONObject.numberToString((Number)null); - fail("Expected an exception"); - } catch (JSONException e) { - assertEquals("Expecting an exception message", - "Null pointer", - e.getMessage()); - } + JSONParserConfiguration jsonParserConfiguration = new JSONParserConfiguration(); + if (jsonParserConfiguration.isStrictMode()) { + System.out.println("Skipping JSONObjectTest jaonObjectParsingErrors() when strictMode default is true"); + } else { + try { + // does not start with '{' + String str = "abc"; + assertNull("Expected an exception", new JSONObject(str)); + } catch (JSONException e) { + assertEquals("Expecting an exception message", + "A JSONObject text must begin with '{' at 1 [character 2 line 1]", + e.getMessage()); + } + try { + // does not end with '}' + String str = "{"; + assertNull("Expected an exception", new JSONObject(str)); + } catch (JSONException e) { + assertEquals("Expecting an exception message", + "A JSONObject text must end with '}' at 1 [character 2 line 1]", + e.getMessage()); + } + try { + // key with no ':' + String str = "{\"myKey\" = true}"; + assertNull("Expected an exception", new JSONObject(str)); + } catch (JSONException e) { + assertEquals("Expecting an exception message", + "Expected a ':' after a key at 10 [character 11 line 1]", + e.getMessage()); + } + try { + // entries with no ',' separator + String str = "{\"myKey\":true \"myOtherKey\":false}"; + assertNull("Expected an exception", new JSONObject(str)); + } catch (JSONException e) { + assertEquals("Expecting an exception message", + "Expected a ',' or '}' at 15 [character 16 line 1]", + e.getMessage()); + } + try { + // key is a nested map + String str = "{{\"foo\": \"bar\"}: \"baz\"}"; + assertNull("Expected an exception", new JSONObject(str)); + } catch (JSONException e) { + assertEquals("Expecting an exception message", + "Missing value at 1 [character 2 line 1]", + e.getMessage()); + } + try { + // key is a nested array containing a map + String str = "{\"a\": 1, [{\"foo\": \"bar\"}]: \"baz\"}"; + assertNull("Expected an exception", new JSONObject(str)); + } catch (JSONException e) { + assertEquals("Expecting an exception message", + "Missing value at 9 [character 10 line 1]", + e.getMessage()); + } + try { + // key contains } + String str = "{foo}: 2}"; + assertNull("Expected an exception", new JSONObject(str)); + } catch (JSONException e) { + assertEquals("Expecting an exception message", + "Expected a ':' after a key at 5 [character 6 line 1]", + e.getMessage()); + } + try { + // key contains ] + String str = "{foo]: 2}"; + assertNull("Expected an exception", new JSONObject(str)); + } catch (JSONException e) { + assertEquals("Expecting an exception message", + "Expected a ':' after a key at 5 [character 6 line 1]", + e.getMessage()); + } + try { + // \0 after , + String str = "{\"myKey\":true, \0\"myOtherKey\":false}"; + assertNull("Expected an exception", new JSONObject(str)); + } catch (JSONException e) { + assertEquals("Expecting an exception message", + "A JSONObject text must end with '}' at 15 [character 16 line 1]", + e.getMessage()); + } + try { + // append to wrong key + String str = "{\"myKey\":true, \"myOtherKey\":false}"; + JSONObject jsonObject = new JSONObject(str); + jsonObject.append("myKey", "hello"); + fail("Expected an exception"); + } catch (JSONException e) { + assertEquals("Expecting an exception message", + "JSONObject[\"myKey\"] is not a JSONArray (null).", + e.getMessage()); + } + try { + // increment wrong key + String str = "{\"myKey\":true, \"myOtherKey\":false}"; + JSONObject jsonObject = new JSONObject(str); + jsonObject.increment("myKey"); + fail("Expected an exception"); + } catch (JSONException e) { + assertEquals("Expecting an exception message", + "Unable to increment [\"myKey\"].", + e.getMessage()); + } + try { + // invalid key + String str = "{\"myKey\":true, \"myOtherKey\":false}"; + JSONObject jsonObject = new JSONObject(str); + jsonObject.get(null); + fail("Expected an exception"); + } catch (JSONException e) { + assertEquals("Expecting an exception message", + "Null key.", + e.getMessage()); + } + try { + // invalid numberToString() + JSONObject.numberToString((Number) null); + fail("Expected an exception"); + } catch (JSONException e) { + assertEquals("Expecting an exception message", + "Null pointer", + e.getMessage()); + } - try { - // multiple putOnce key - JSONObject jsonObject = new JSONObject("{}"); - jsonObject.putOnce("hello", "world"); - jsonObject.putOnce("hello", "world!"); - fail("Expected an exception"); - } catch (JSONException e) { - assertTrue("", true); - } - try { - // test validity of invalid double - JSONObject.testValidity(Double.NaN); - fail("Expected an exception"); - } catch (JSONException e) { - assertTrue("", true); - } - try { - // test validity of invalid float - JSONObject.testValidity(Float.NEGATIVE_INFINITY); - fail("Expected an exception"); - } catch (JSONException e) { - assertTrue("", true); - } - try { - // test exception message when including a duplicate key (level 0) - String str = "{\n" - +" \"attr01\":\"value-01\",\n" - +" \"attr02\":\"value-02\",\n" - +" \"attr03\":\"value-03\",\n" - +" \"attr03\":\"value-04\"\n" + try { + // multiple putOnce key + JSONObject jsonObject = new JSONObject("{}"); + jsonObject.putOnce("hello", "world"); + jsonObject.putOnce("hello", "world!"); + fail("Expected an exception"); + } catch (JSONException e) { + assertTrue("", true); + } + try { + // test validity of invalid double + JSONObject.testValidity(Double.NaN); + fail("Expected an exception"); + } catch (JSONException e) { + assertTrue("", true); + } + try { + // test validity of invalid float + JSONObject.testValidity(Float.NEGATIVE_INFINITY); + fail("Expected an exception"); + } catch (JSONException e) { + assertTrue("", true); + } + try { + // test exception message when including a duplicate key (level 0) + String str = "{\n" + + " \"attr01\":\"value-01\",\n" + + " \"attr02\":\"value-02\",\n" + + " \"attr03\":\"value-03\",\n" + + " \"attr03\":\"value-04\"\n" + "}"; - new JSONObject(str); - fail("Expected an exception"); - } catch (JSONException e) { - assertEquals("Expecting an expection message", - "Duplicate key \"attr03\" at 90 [character 13 line 5]", - e.getMessage()); - } - try { - // test exception message when including a duplicate key (level 0) holding an object - String str = "{\n" - +" \"attr01\":\"value-01\",\n" - +" \"attr02\":\"value-02\",\n" - +" \"attr03\":\"value-03\",\n" - +" \"attr03\": {" - +" \"attr04-01\":\"value-04-01\",n" - +" \"attr04-02\":\"value-04-02\",n" - +" \"attr04-03\":\"value-04-03\"n" + new JSONObject(str); + fail("Expected an exception"); + } catch (JSONException e) { + assertEquals("Expecting an expection message", + "Duplicate key \"attr03\" at 90 [character 13 line 5]", + e.getMessage()); + } + try { + // test exception message when including a duplicate key (level 0) holding an object + String str = "{\n" + + " \"attr01\":\"value-01\",\n" + + " \"attr02\":\"value-02\",\n" + + " \"attr03\":\"value-03\",\n" + + " \"attr03\": {" + + " \"attr04-01\":\"value-04-01\",n" + + " \"attr04-02\":\"value-04-02\",n" + + " \"attr04-03\":\"value-04-03\"n" + " }\n" + "}"; - new JSONObject(str); - fail("Expected an exception"); - } catch (JSONException e) { - assertEquals("Expecting an expection message", - "Duplicate key \"attr03\" at 90 [character 13 line 5]", - e.getMessage()); - } - try { - // test exception message when including a duplicate key (level 0) holding an array - String str = "{\n" - +" \"attr01\":\"value-01\",\n" - +" \"attr02\":\"value-02\",\n" - +" \"attr03\":\"value-03\",\n" - +" \"attr03\": [\n" - +" {" - +" \"attr04-01\":\"value-04-01\",n" - +" \"attr04-02\":\"value-04-02\",n" - +" \"attr04-03\":\"value-04-03\"n" - +" }\n" + new JSONObject(str); + fail("Expected an exception"); + } catch (JSONException e) { + assertEquals("Expecting an expection message", + "Duplicate key \"attr03\" at 90 [character 13 line 5]", + e.getMessage()); + } + try { + // test exception message when including a duplicate key (level 0) holding an array + String str = "{\n" + + " \"attr01\":\"value-01\",\n" + + " \"attr02\":\"value-02\",\n" + + " \"attr03\":\"value-03\",\n" + + " \"attr03\": [\n" + + " {" + + " \"attr04-01\":\"value-04-01\",n" + + " \"attr04-02\":\"value-04-02\",n" + + " \"attr04-03\":\"value-04-03\"n" + + " }\n" + " ]\n" + "}"; - new JSONObject(str); - fail("Expected an exception"); - } catch (JSONException e) { - assertEquals("Expecting an expection message", - "Duplicate key \"attr03\" at 90 [character 13 line 5]", - e.getMessage()); - } - try { - // test exception message when including a duplicate key (level 1) - String str = "{\n" - +" \"attr01\":\"value-01\",\n" - +" \"attr02\":\"value-02\",\n" - +" \"attr03\":\"value-03\",\n" - +" \"attr04\": {\n" - +" \"attr04-01\":\"value04-01\",\n" - +" \"attr04-02\":\"value04-02\",\n" - +" \"attr04-03\":\"value04-03\",\n" - +" \"attr04-03\":\"value04-04\"\n" + new JSONObject(str); + fail("Expected an exception"); + } catch (JSONException e) { + assertEquals("Expecting an expection message", + "Duplicate key \"attr03\" at 90 [character 13 line 5]", + e.getMessage()); + } + try { + // test exception message when including a duplicate key (level 1) + String str = "{\n" + + " \"attr01\":\"value-01\",\n" + + " \"attr02\":\"value-02\",\n" + + " \"attr03\":\"value-03\",\n" + + " \"attr04\": {\n" + + " \"attr04-01\":\"value04-01\",\n" + + " \"attr04-02\":\"value04-02\",\n" + + " \"attr04-03\":\"value04-03\",\n" + + " \"attr04-03\":\"value04-04\"\n" + " }\n" + "}"; - new JSONObject(str); - fail("Expected an exception"); - } catch (JSONException e) { - assertEquals("Expecting an expection message", - "Duplicate key \"attr04-03\" at 215 [character 20 line 9]", - e.getMessage()); - } - try { - // test exception message when including a duplicate key (level 1) holding an object - String str = "{\n" - +" \"attr01\":\"value-01\",\n" - +" \"attr02\":\"value-02\",\n" - +" \"attr03\":\"value-03\",\n" - +" \"attr04\": {\n" - +" \"attr04-01\":\"value04-01\",\n" - +" \"attr04-02\":\"value04-02\",\n" - +" \"attr04-03\":\"value04-03\",\n" - +" \"attr04-03\": {\n" - +" \"attr04-04-01\":\"value04-04-01\",\n" - +" \"attr04-04-02\":\"value04-04-02\",\n" - +" \"attr04-04-03\":\"value04-04-03\",\n" - +" }\n" - +" }\n" + new JSONObject(str); + fail("Expected an exception"); + } catch (JSONException e) { + assertEquals("Expecting an expection message", + "Duplicate key \"attr04-03\" at 215 [character 20 line 9]", + e.getMessage()); + } + try { + // test exception message when including a duplicate key (level 1) holding an object + String str = "{\n" + + " \"attr01\":\"value-01\",\n" + + " \"attr02\":\"value-02\",\n" + + " \"attr03\":\"value-03\",\n" + + " \"attr04\": {\n" + + " \"attr04-01\":\"value04-01\",\n" + + " \"attr04-02\":\"value04-02\",\n" + + " \"attr04-03\":\"value04-03\",\n" + + " \"attr04-03\": {\n" + + " \"attr04-04-01\":\"value04-04-01\",\n" + + " \"attr04-04-02\":\"value04-04-02\",\n" + + " \"attr04-04-03\":\"value04-04-03\",\n" + + " }\n" + + " }\n" + "}"; - new JSONObject(str); - fail("Expected an exception"); - } catch (JSONException e) { - assertEquals("Expecting an expection message", - "Duplicate key \"attr04-03\" at 215 [character 20 line 9]", - e.getMessage()); - } - try { - // test exception message when including a duplicate key (level 1) holding an array - String str = "{\n" - +" \"attr01\":\"value-01\",\n" - +" \"attr02\":\"value-02\",\n" - +" \"attr03\":\"value-03\",\n" - +" \"attr04\": {\n" - +" \"attr04-01\":\"value04-01\",\n" - +" \"attr04-02\":\"value04-02\",\n" - +" \"attr04-03\":\"value04-03\",\n" - +" \"attr04-03\": [\n" - +" {\n" - +" \"attr04-04-01\":\"value04-04-01\",\n" - +" \"attr04-04-02\":\"value04-04-02\",\n" - +" \"attr04-04-03\":\"value04-04-03\",\n" - +" }\n" - +" ]\n" - +" }\n" + new JSONObject(str); + fail("Expected an exception"); + } catch (JSONException e) { + assertEquals("Expecting an expection message", + "Duplicate key \"attr04-03\" at 215 [character 20 line 9]", + e.getMessage()); + } + try { + // test exception message when including a duplicate key (level 1) holding an array + String str = "{\n" + + " \"attr01\":\"value-01\",\n" + + " \"attr02\":\"value-02\",\n" + + " \"attr03\":\"value-03\",\n" + + " \"attr04\": {\n" + + " \"attr04-01\":\"value04-01\",\n" + + " \"attr04-02\":\"value04-02\",\n" + + " \"attr04-03\":\"value04-03\",\n" + + " \"attr04-03\": [\n" + + " {\n" + + " \"attr04-04-01\":\"value04-04-01\",\n" + + " \"attr04-04-02\":\"value04-04-02\",\n" + + " \"attr04-04-03\":\"value04-04-03\",\n" + + " }\n" + + " ]\n" + + " }\n" + "}"; - new JSONObject(str); - fail("Expected an exception"); - } catch (JSONException e) { - assertEquals("Expecting an expection message", - "Duplicate key \"attr04-03\" at 215 [character 20 line 9]", - e.getMessage()); - } - try { - // test exception message when including a duplicate key in object (level 0) within an array - String str = "[\n" - +" {\n" - +" \"attr01\":\"value-01\",\n" - +" \"attr02\":\"value-02\"\n" - +" },\n" - +" {\n" - +" \"attr01\":\"value-01\",\n" - +" \"attr01\":\"value-02\"\n" - +" }\n" + new JSONObject(str); + fail("Expected an exception"); + } catch (JSONException e) { + assertEquals("Expecting an expection message", + "Duplicate key \"attr04-03\" at 215 [character 20 line 9]", + e.getMessage()); + } + try { + // test exception message when including a duplicate key in object (level 0) within an array + String str = "[\n" + + " {\n" + + " \"attr01\":\"value-01\",\n" + + " \"attr02\":\"value-02\"\n" + + " },\n" + + " {\n" + + " \"attr01\":\"value-01\",\n" + + " \"attr01\":\"value-02\"\n" + + " }\n" + "]"; - new JSONArray(str); - fail("Expected an exception"); - } catch (JSONException e) { - assertEquals("Expecting an expection message", - "Duplicate key \"attr01\" at 124 [character 17 line 8]", - e.getMessage()); - } - try { - // test exception message when including a duplicate key in object (level 1) within an array - String str = "[\n" - +" {\n" - +" \"attr01\":\"value-01\",\n" - +" \"attr02\": {\n" - +" \"attr02-01\":\"value-02-01\",\n" - +" \"attr02-02\":\"value-02-02\"\n" - +" }\n" - +" },\n" - +" {\n" - +" \"attr01\":\"value-01\",\n" - +" \"attr02\": {\n" - +" \"attr02-01\":\"value-02-01\",\n" - +" \"attr02-01\":\"value-02-02\"\n" - +" }\n" - +" }\n" + new JSONArray(str); + fail("Expected an exception"); + } catch (JSONException e) { + assertEquals("Expecting an expection message", + "Duplicate key \"attr01\" at 124 [character 17 line 8]", + e.getMessage()); + } + try { + // test exception message when including a duplicate key in object (level 1) within an array + String str = "[\n" + + " {\n" + + " \"attr01\":\"value-01\",\n" + + " \"attr02\": {\n" + + " \"attr02-01\":\"value-02-01\",\n" + + " \"attr02-02\":\"value-02-02\"\n" + + " }\n" + + " },\n" + + " {\n" + + " \"attr01\":\"value-01\",\n" + + " \"attr02\": {\n" + + " \"attr02-01\":\"value-02-01\",\n" + + " \"attr02-01\":\"value-02-02\"\n" + + " }\n" + + " }\n" + "]"; - new JSONArray(str); - fail("Expected an exception"); - } catch (JSONException e) { - assertEquals("Expecting an expection message", - "Duplicate key \"attr02-01\" at 269 [character 24 line 13]", - e.getMessage()); + new JSONArray(str); + fail("Expected an exception"); + } catch (JSONException e) { + assertEquals("Expecting an expection message", + "Duplicate key \"attr02-01\" at 269 [character 24 line 13]", + e.getMessage()); + } } } @@ -3815,21 +3830,21 @@ public class JSONObjectTest { // Behavior documented in #826 JSONObject parsing 0-led numeric strings as ints // After reverting the code, personId is stored as a string, and the behavior is as expected - String personId = "0123"; - JSONObject j1 = new JSONObject("{personId: " + personId + "}"); + String personId = "\"0123\""; + JSONObject j1 = new JSONObject("{\"personId\": " + personId + "}"); assertEquals(j1.getString("personId"), "0123"); // Also #826. Here is input with missing quotes. Because of the leading zero, it should not be parsed as a number. // This example was mentioned in the same ticket // After reverting the code, personId is stored as a string, and the behavior is as expected - JSONObject j2 = new JSONObject("{\"personId\":0123}"); + JSONObject j2 = new JSONObject("{\"personId\":\"0123\"}"); assertEquals(j2.getString("personId"), "0123"); // Behavior uncovered while working on the code // All of the values are stored as strings except for hex4, which is stored as a number. This is probably incorrect JSONObject j3 = new JSONObject("{ " + "\"hex1\": \"010e4\", \"hex2\": \"00f0\", \"hex3\": \"0011\", " + - "\"hex4\": 00e0, \"hex5\": 00f0, \"hex6\": 0011 }"); + "\"hex4\": 00e0, \"hex5\": \"00f0\", \"hex6\": \"0011\" }"); assertEquals(j3.getString("hex1"), "010e4"); assertEquals(j3.getString("hex2"), "00f0"); assertEquals(j3.getString("hex3"), "0011"); diff --git a/src/test/java/org/json/junit/JSONPointerTest.java b/src/test/java/org/json/junit/JSONPointerTest.java index 45c7dbd..a420b29 100644 --- a/src/test/java/org/json/junit/JSONPointerTest.java +++ b/src/test/java/org/json/junit/JSONPointerTest.java @@ -384,8 +384,7 @@ public class JSONPointerTest { String str = "{"+ "\"string\\\\\\\\Key\":\"hello world!\","+ - "\"\\\\\":\"slash test\"," + - "}"+ + "\"\\\\\":\"slash test\"" + "}"; JSONObject jsonObject = new JSONObject(str); //Summary of issue: When a KEY in the jsonObject is "\\\\" --> it's held diff --git a/src/test/java/org/json/junit/XMLConfigurationTest.java b/src/test/java/org/json/junit/XMLConfigurationTest.java index e9714af..867f0fa 100755 --- a/src/test/java/org/json/junit/XMLConfigurationTest.java +++ b/src/test/java/org/json/junit/XMLConfigurationTest.java @@ -270,9 +270,9 @@ public class XMLConfigurationTest { String expectedStr = "{\"addresses\":{\"address\":{\"street\":\"[CDATA[Baker street 5]\","+ - "\"name\":\"Joe Tester\",\"NothingHere\":\"\",TrueValue:true,\n"+ + "\"name\":\"Joe Tester\",\"NothingHere\":\"\",\"TrueValue\":true,\n"+ "\"FalseValue\":false,\"NullValue\":null,\"PositiveValue\":42,\n"+ - "\"NegativeValue\":-23,\"DoubleValue\":-23.45,\"Nan\":-23x.45,\n"+ + "\"NegativeValue\":-23,\"DoubleValue\":-23.45,\"Nan\":\"-23x.45\",\n"+ "\"ArrayOfNum\":\"1, 2, 3, 4.1, 5.2\"\n"+ "},\"xsi:noNamespaceSchemaLocation\":"+ "\"test.xsd\",\"xmlns:xsi\":\"http://www.w3.org/2001/"+ diff --git a/src/test/java/org/json/junit/XMLTest.java b/src/test/java/org/json/junit/XMLTest.java index 3b26b22..2fa5dae 100644 --- a/src/test/java/org/json/junit/XMLTest.java +++ b/src/test/java/org/json/junit/XMLTest.java @@ -267,9 +267,9 @@ public class XMLTest { String expectedStr = "{\"addresses\":{\"address\":{\"street\":\"[CDATA[Baker street 5]\","+ - "\"name\":\"Joe Tester\",\"NothingHere\":\"\",TrueValue:true,\n"+ + "\"name\":\"Joe Tester\",\"NothingHere\":\"\",\"TrueValue\":true,\n"+ "\"FalseValue\":false,\"NullValue\":null,\"PositiveValue\":42,\n"+ - "\"NegativeValue\":-23,\"DoubleValue\":-23.45,\"Nan\":-23x.45,\n"+ + "\"NegativeValue\":-23,\"DoubleValue\":-23.45,\"Nan\":\"-23x.45\",\n"+ "\"ArrayOfNum\":\"1, 2, 3, 4.1, 5.2\"\n"+ "},\"xsi:noNamespaceSchemaLocation\":"+ "\"test.xsd\",\"xmlns:xsi\":\"http://www.w3.org/2001/"+ @@ -1180,7 +1180,7 @@ public class XMLTest { @Test public void shouldCreateExplicitEndTagWithEmptyValueWhenConfigured(){ - String jsonString = "{outer:{innerOne:\"\", innerTwo:\"two\"}}"; + String jsonString = "{\"outer\":{\"innerOne\":\"\", \"innerTwo\":\"two\"}}"; JSONObject jsonObject = new JSONObject(jsonString); String expectedXmlString = "two"; String xmlForm = XML.toString(jsonObject,"encloser", new XMLParserConfiguration().withCloseEmptyTag(true)); @@ -1191,7 +1191,7 @@ public class XMLTest { @Test public void shouldNotCreateExplicitEndTagWithEmptyValueWhenNotConfigured(){ - String jsonString = "{outer:{innerOne:\"\", innerTwo:\"two\"}}"; + String jsonString = "{\"outer\":{\"innerOne\":\"\", \"innerTwo\":\"two\"}}"; JSONObject jsonObject = new JSONObject(jsonString); String expectedXmlString = "two"; String xmlForm = XML.toString(jsonObject,"encloser", new XMLParserConfiguration().withCloseEmptyTag(false)); From 2dcef89a6f3a79a0efc2c1e260029a5b26c646a9 Mon Sep 17 00:00:00 2001 From: Sean Leary Date: Sat, 21 Dec 2024 09:50:52 -0600 Subject: [PATCH 6/6] Code review action items - add comments and consistent error messages for strict mode --- src/main/java/org/json/JSONArray.java | 32 ++++++++------ src/main/java/org/json/JSONObject.java | 21 ++++++---- .../org/json/JSONParserConfiguration.java | 20 +++++---- src/main/java/org/json/JSONTokener.java | 7 +++- .../java/org/json/junit/JSONArrayTest.java | 2 +- .../java/org/json/junit/JSONObjectTest.java | 2 +- .../junit/JSONParserConfigurationTest.java | 42 ++++++++++++------- 7 files changed, 77 insertions(+), 49 deletions(-) diff --git a/src/main/java/org/json/JSONArray.java b/src/main/java/org/json/JSONArray.java index 759acd7..6458ab2 100644 --- a/src/main/java/org/json/JSONArray.java +++ b/src/main/java/org/json/JSONArray.java @@ -67,8 +67,10 @@ public class JSONArray implements Iterable { */ private final ArrayList myArrayList; + // strict mode checks after constructor require access to this object private JSONTokener jsonTokener; + // strict mode checks after constructor require access to this object private JSONParserConfiguration jsonParserConfiguration; /** @@ -138,8 +140,16 @@ public class JSONArray implements Iterable { throw x.syntaxError("Expected a ',' or ']'"); } if (nextChar == ']') { + // trailing commas are not allowed in strict mode if (jsonParserConfiguration.isStrictMode()) { - throw x.syntaxError("Expected another array element"); + throw x.syntaxError("Strict mode error: Expected another array element"); + } + return; + } + if (nextChar == ',') { + // consecutive commas are not allowed in strict mode + if (jsonParserConfiguration.isStrictMode()) { + throw x.syntaxError("Strict mode error: Expected a valid array element"); } return; } @@ -166,12 +176,10 @@ public class JSONArray implements Iterable { */ public JSONArray(String source) throws JSONException { this(source, new JSONParserConfiguration()); - if (this.jsonParserConfiguration.isStrictMode()) { - char c = jsonTokener.nextClean(); - if (c != 0) { - throw jsonTokener.syntaxError(String.format("invalid character '%s' found after end of array", c)); - - } + // Strict mode does not allow trailing chars + if (this.jsonParserConfiguration.isStrictMode() && + this.jsonTokener.nextClean() != 0) { + throw jsonTokener.syntaxError("Strict mode error: Unparsed characters found at end of input text"); } } @@ -188,12 +196,10 @@ public class JSONArray implements Iterable { */ public JSONArray(String source, JSONParserConfiguration jsonParserConfiguration) throws JSONException { this(new JSONTokener(source), jsonParserConfiguration); - if (this.jsonParserConfiguration.isStrictMode()) { - char c = jsonTokener.nextClean(); - if (c != 0) { - throw jsonTokener.syntaxError(String.format("invalid character '%s' found after end of array", c)); - - } + // Strict mode does not allow trailing chars + if (this.jsonParserConfiguration.isStrictMode() && + this.jsonTokener.nextClean() != 0) { + throw jsonTokener.syntaxError("Strict mode error: Unparsed characters found at end of input text"); } } diff --git a/src/main/java/org/json/JSONObject.java b/src/main/java/org/json/JSONObject.java index f49bab3..3bb6da7 100644 --- a/src/main/java/org/json/JSONObject.java +++ b/src/main/java/org/json/JSONObject.java @@ -152,8 +152,10 @@ public class JSONObject { */ public static final Object NULL = new Null(); + // strict mode checks after constructor require access to this object private JSONTokener jsonTokener; + // strict mode checks after constructor require access to this object private JSONParserConfiguration jsonParserConfiguration; /** @@ -268,13 +270,15 @@ public class JSONObject { switch (x.nextClean()) { case ';': + // In strict mode semicolon is not a valid separator if (jsonParserConfiguration.isStrictMode()) { - throw x.syntaxError("Invalid character ';' found in object in strict mode"); + throw x.syntaxError("Strict mode error: Invalid character ';' found"); } case ',': if (x.nextClean() == '}') { + // trailing commas are not allowed in strict mode if (jsonParserConfiguration.isStrictMode()) { - throw x.syntaxError("Expected another object element"); + throw x.syntaxError("Strict mode error: Expected another object element"); } return; } @@ -452,9 +456,10 @@ public class JSONObject { */ public JSONObject(String source) throws JSONException { this(source, new JSONParserConfiguration()); + // Strict mode does not allow trailing chars if (this.jsonParserConfiguration.isStrictMode() && this.jsonTokener.nextClean() != 0) { - throw new JSONException("Unparsed characters found at end of input text"); + throw new JSONException("Strict mode error: Unparsed characters found at end of input text"); } } @@ -474,12 +479,10 @@ public class JSONObject { */ public JSONObject(String source, JSONParserConfiguration jsonParserConfiguration) throws JSONException { this(new JSONTokener(source), jsonParserConfiguration); - if (this.jsonParserConfiguration.isStrictMode()) { - char c = jsonTokener.nextClean(); - if (c != 0) { - throw jsonTokener.syntaxError(String.format("invalid character '%s' found after end of array", c)); - - } + // Strict mode does not allow trailing chars + if (this.jsonParserConfiguration.isStrictMode() && + this.jsonTokener.nextClean() != 0) { + throw new JSONException("Strict mode error: Unparsed characters found at end of input text"); } } diff --git a/src/main/java/org/json/JSONParserConfiguration.java b/src/main/java/org/json/JSONParserConfiguration.java index 46996cb..f5d0660 100644 --- a/src/main/java/org/json/JSONParserConfiguration.java +++ b/src/main/java/org/json/JSONParserConfiguration.java @@ -64,6 +64,18 @@ public class JSONParserConfiguration extends ParserConfiguration { return clone; } + /** + * Sets the strict mode configuration for the JSON parser with default true value + *

+ * When strict mode is enabled, the parser will throw a JSONException if it encounters an invalid character + * immediately following the final ']' character in the input. This is useful for ensuring strict adherence to the + * JSON syntax, as any characters after the final closing bracket of a JSON array are considered invalid. + * @return a new JSONParserConfiguration instance with the updated strict mode setting + */ + public JSONParserConfiguration withStrictMode() { + return withStrictMode(true); + } + /** * Sets the strict mode configuration for the JSON parser. *

@@ -92,13 +104,7 @@ public class JSONParserConfiguration extends ParserConfiguration { } /** - * Retrieves the current strict mode setting of the JSON parser. - *

- * Strict mode, when enabled, instructs the parser to throw a JSONException if it encounters an invalid character - * immediately following the final ']' character in the input. This ensures strict adherence to the JSON syntax, as - * any characters after the final closing bracket of a JSON array are considered invalid. - * - * @return the current strict mode setting. True if strict mode is enabled, false otherwise. + * @return the current strict mode setting. */ public boolean isStrictMode() { return this.strictMode; diff --git a/src/main/java/org/json/JSONTokener.java b/src/main/java/org/json/JSONTokener.java index 2fcc24a..d4c780e 100644 --- a/src/main/java/org/json/JSONTokener.java +++ b/src/main/java/org/json/JSONTokener.java @@ -32,6 +32,7 @@ public class JSONTokener { /** the number of characters read in the previous line. */ private long characterPreviousLine; + // access to this object is required for strict mode checking private JSONParserConfiguration jsonParserConfiguration; /** @@ -443,10 +444,11 @@ public class JSONTokener { Object nextSimpleValue(char c) { String string; + // Strict mode only allows strings with explicit double quotes if (jsonParserConfiguration != null && jsonParserConfiguration.isStrictMode() && c == '\'') { - throw this.syntaxError("Single quote wrap not allowed in strict mode"); + throw this.syntaxError("Strict mode error: Single quoted strings are not allowed"); } switch (c) { case '"': @@ -477,10 +479,11 @@ public class JSONTokener { throw this.syntaxError("Missing value"); } Object obj = JSONObject.stringToValue(string); + // Strict mode only allows strings with explicit double quotes if (jsonParserConfiguration != null && jsonParserConfiguration.isStrictMode() && obj instanceof String) { - throw this.syntaxError(String.format("Value '%s' is not surrounded by quotes", obj)); + throw this.syntaxError(String.format("Strict mode error: Value '%s' is not surrounded by quotes", obj)); } return obj; } diff --git a/src/test/java/org/json/junit/JSONArrayTest.java b/src/test/java/org/json/junit/JSONArrayTest.java index ed7c9ba..e6d7147 100644 --- a/src/test/java/org/json/junit/JSONArrayTest.java +++ b/src/test/java/org/json/junit/JSONArrayTest.java @@ -481,7 +481,7 @@ public class JSONArrayTest { System.out.println("Skipping JSONArrayTest unquotedText() when strictMode default is true"); } else { String str = "[value1, something!, (parens), foo@bar.com, 23, 23+45]"; - JSONArray jsonArray = new JSONArray(str); + JSONArray jsonArray = new JSONArray(str); List expected = Arrays.asList("value1", "something!", "(parens)", "foo@bar.com", 23, "23+45"); assertEquals(expected, jsonArray.toList()); } diff --git a/src/test/java/org/json/junit/JSONObjectTest.java b/src/test/java/org/json/junit/JSONObjectTest.java index ad4974b..7f121f3 100644 --- a/src/test/java/org/json/junit/JSONObjectTest.java +++ b/src/test/java/org/json/junit/JSONObjectTest.java @@ -83,7 +83,7 @@ public class JSONObjectTest { Singleton.getInstance().setSomeInt(0); Singleton.getInstance().setSomeString(null); } - + /** * Tests that the similar method is working as expected. */ diff --git a/src/test/java/org/json/junit/JSONParserConfigurationTest.java b/src/test/java/org/json/junit/JSONParserConfigurationTest.java index 84c3911..422c90c 100644 --- a/src/test/java/org/json/junit/JSONParserConfigurationTest.java +++ b/src/test/java/org/json/junit/JSONParserConfigurationTest.java @@ -184,7 +184,8 @@ public class JSONParserConfigurationTest { .withStrictMode(true); String testCase = "[badString]"; JSONException je = assertThrows(JSONException.class, () -> new JSONArray(testCase, jsonParserConfiguration)); - assertEquals("Value 'badString' is not surrounded by quotes at 10 [character 11 line 1]", je.getMessage()); + assertEquals("Strict mode error: Value 'badString' is not surrounded by quotes at 10 [character 11 line 1]", + je.getMessage()); } @Test @@ -193,7 +194,8 @@ public class JSONParserConfigurationTest { .withStrictMode(true); String testCase = "{\"a0\":badString}"; JSONException je = assertThrows(JSONException.class, () -> new JSONObject(testCase, jsonParserConfiguration)); - assertEquals("Value 'badString' is not surrounded by quotes at 15 [character 16 line 1]", je.getMessage()); + assertEquals("Strict mode error: Value 'badString' is not surrounded by quotes at 15 [character 16 line 1]", + je.getMessage()); } @Test @@ -289,7 +291,8 @@ public class JSONParserConfigurationTest { String testCase = "[1,2];[3,4]"; JSONException je = assertThrows("expected non-compliant array but got instead: " + testCase, JSONException.class, () -> new JSONArray(testCase, jsonParserConfiguration)); - assertEquals("invalid character ';' found after end of array at 6 [character 7 line 1]", je.getMessage()); + assertEquals("Strict mode error: Unparsed characters found at end of input text at 6 [character 7 line 1]", + je.getMessage()); } @Test @@ -299,7 +302,7 @@ public class JSONParserConfigurationTest { String testCase = "{\"a0\":[1,2];\"a1\":[3,4]}"; JSONException je = assertThrows("expected non-compliant array but got instead: " + testCase, JSONException.class, () -> new JSONObject(testCase, jsonParserConfiguration)); - assertEquals("Invalid character ';' found in object in strict mode at 12 [character 13 line 1]", je.getMessage()); + assertEquals("Strict mode error: Invalid character ';' found at 12 [character 13 line 1]", je.getMessage()); } @Test @@ -309,7 +312,8 @@ public class JSONParserConfigurationTest { String testCase = "[\"1\",\"2\"];[3,4]"; JSONException je = assertThrows("expected non-compliant array but got instead: " + testCase, JSONException.class, () -> new JSONArray(testCase, jsonParserConfiguration)); - assertEquals("invalid character ';' found after end of array at 10 [character 11 line 1]", je.getMessage()); + assertEquals("Strict mode error: Unparsed characters found at end of input text at 10 [character 11 line 1]", + je.getMessage()); } @Test @@ -319,7 +323,7 @@ public class JSONParserConfigurationTest { String testCase = "{\"a0\":[\"1\",\"2\"];\"a1\":[3,4]}"; JSONException je = assertThrows("expected non-compliant array but got instead: " + testCase, JSONException.class, () -> new JSONObject(testCase, jsonParserConfiguration)); - assertEquals("Invalid character ';' found in object in strict mode at 16 [character 17 line 1]", je.getMessage()); + assertEquals("Strict mode error: Invalid character ';' found at 16 [character 17 line 1]", je.getMessage()); } @Test @@ -329,7 +333,8 @@ public class JSONParserConfigurationTest { String testCase = "[{\"test\": implied}]"; JSONException je = assertThrows("expected non-compliant array but got instead: " + testCase, JSONException.class, () -> new JSONArray(testCase, jsonParserConfiguration)); - assertEquals("Value 'implied' is not surrounded by quotes at 17 [character 18 line 1]", je.getMessage()); + assertEquals("Strict mode error: Value 'implied' is not surrounded by quotes at 17 [character 18 line 1]", + je.getMessage()); } @Test @@ -339,7 +344,8 @@ public class JSONParserConfigurationTest { String testCase = "{\"a0\":{\"test\": implied}]}"; JSONException je = assertThrows("expected non-compliant array but got instead: " + testCase, JSONException.class, () -> new JSONObject(testCase, jsonParserConfiguration)); - assertEquals("Value 'implied' is not surrounded by quotes at 22 [character 23 line 1]", je.getMessage()); + assertEquals("Strict mode error: Value 'implied' is not surrounded by quotes at 22 [character 23 line 1]", + je.getMessage()); } @Test @@ -381,13 +387,13 @@ public class JSONParserConfigurationTest { "Expected a ',' or ']' at 10 [character 11 line 1]", jeOne.getMessage()); assertEquals( - "Single quote wrap not allowed in strict mode at 2 [character 3 line 1]", + "Strict mode error: Single quoted strings are not allowed at 2 [character 3 line 1]", jeTwo.getMessage()); assertEquals( - "Single quote wrap not allowed in strict mode at 2 [character 3 line 1]", + "Strict mode error: Single quoted strings are not allowed at 2 [character 3 line 1]", jeThree.getMessage()); assertEquals( - "Single quote wrap not allowed in strict mode at 3 [character 4 line 1]", + "Strict mode error: Single quoted strings are not allowed at 3 [character 4 line 1]", jeFour.getMessage()); } @@ -414,13 +420,13 @@ public class JSONParserConfigurationTest { "Expected a ':' after a key at 10 [character 11 line 1]", jeOne.getMessage()); assertEquals( - "Single quote wrap not allowed in strict mode at 2 [character 3 line 1]", + "Strict mode error: Single quoted strings are not allowed at 2 [character 3 line 1]", jeTwo.getMessage()); assertEquals( - "Single quote wrap not allowed in strict mode at 6 [character 7 line 1]", + "Strict mode error: Single quoted strings are not allowed at 6 [character 7 line 1]", jeThree.getMessage()); assertEquals( - "Single quote wrap not allowed in strict mode at 2 [character 3 line 1]", + "Strict mode error: Single quoted strings are not allowed at 2 [character 3 line 1]", jeFour.getMessage()); } @@ -467,7 +473,8 @@ public class JSONParserConfigurationTest { JSONException je = assertThrows("expected non-compliant array but got instead: " + testCase, JSONException.class, () -> new JSONArray(testCase, jsonParserConfiguration)); - assertEquals("Value 'test' is not surrounded by quotes at 6 [character 7 line 1]", je.getMessage()); + assertEquals("Strict mode error: Value 'test' is not surrounded by quotes at 6 [character 7 line 1]", + je.getMessage()); } @Test @@ -479,7 +486,8 @@ public class JSONParserConfigurationTest { JSONException je = assertThrows("expected non-compliant json but got instead: " + testCase, JSONException.class, () -> new JSONObject(testCase, jsonParserConfiguration)); - assertEquals("Value 'test' is not surrounded by quotes at 5 [character 6 line 1]", je.getMessage()); + assertEquals("Strict mode error: Value 'test' is not surrounded by quotes at 5 [character 6 line 1]", + je.getMessage()); } /** @@ -492,6 +500,8 @@ public class JSONParserConfigurationTest { return Arrays.asList( "[1],", "[1,]", + "[,]", + "[,,]", "[[1],\"sa\",[2]]a", "[1],\"dsa\": \"test\"", "[[a]]",