From 0ae8d5ab4f19d20bbfe0ba02c62a1df9243f8d31 Mon Sep 17 00:00:00 2001 From: sajad mahmoodifar Date: Fri, 18 Sep 2026 13:25:36 +0330 Subject: [PATCH 1/2] fix: apply explicit parser configuration Fixes #945 --- src/main/java/org/json/JSONArray.java | 3 ++ src/main/java/org/json/JSONObject.java | 3 ++ .../junit/JSONParserConfigurationTest.java | 51 +++++++++++++++++++ 3 files changed, 57 insertions(+) diff --git a/src/main/java/org/json/JSONArray.java b/src/main/java/org/json/JSONArray.java index 2bd80c1d2..a0934e6d9 100644 --- a/src/main/java/org/json/JSONArray.java +++ b/src/main/java/org/json/JSONArray.java @@ -88,6 +88,8 @@ public JSONArray(JSONTokener x) throws JSONException { /** * Constructs a JSONArray from a JSONTokener and a JSONParserConfiguration. + * The supplied configuration replaces the tokener's configuration and applies + * to all values, including nested objects and arrays. * * @param x A JSONTokener instance from which the JSONArray is constructed. * @param jsonParserConfiguration A JSONParserConfiguration instance that controls the behavior of the parser. @@ -108,6 +110,7 @@ public JSONArray(JSONTokener x, JSONParserConfiguration jsonParserConfiguration) */ JSONArray(JSONTokener x, JSONParserConfiguration jsonParserConfiguration, boolean isInitial) throws JSONException { this(); + x.setJsonParserConfiguration(jsonParserConfiguration); if (x.nextClean() != '[') { throw x.syntaxError("A JSONArray text must start with '['"); diff --git a/src/main/java/org/json/JSONObject.java b/src/main/java/org/json/JSONObject.java index 0b2268caa..77571f73d 100644 --- a/src/main/java/org/json/JSONObject.java +++ b/src/main/java/org/json/JSONObject.java @@ -204,6 +204,8 @@ public JSONObject(JSONTokener x) throws JSONException { /** * Construct a JSONObject from a JSONTokener with custom json parse configurations. + * The supplied configuration replaces the tokener's configuration and applies + * to all values, including nested objects and arrays. * * @param x * A JSONTokener object containing the source string. @@ -233,6 +235,7 @@ public JSONObject(JSONTokener x, JSONParserConfiguration jsonParserConfiguration */ JSONObject(JSONTokener x, JSONParserConfiguration jsonParserConfiguration, boolean isInitial) throws JSONException { this(); + x.setJsonParserConfiguration(jsonParserConfiguration); if (x.nextClean() != '{') { throw x.syntaxError("A JSONObject text must begin with '{'"); diff --git a/src/test/java/org/json/junit/JSONParserConfigurationTest.java b/src/test/java/org/json/junit/JSONParserConfigurationTest.java index 926c49f41..cbe44474e 100644 --- a/src/test/java/org/json/junit/JSONParserConfigurationTest.java +++ b/src/test/java/org/json/junit/JSONParserConfigurationTest.java @@ -16,6 +16,7 @@ import java.util.stream.Stream; import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertSame; import static org.junit.Assert.assertThrows; import static org.junit.Assert.assertTrue; import static org.junit.Assert.fail; @@ -36,6 +37,56 @@ public void testOverwrite() { assertEquals("duplicate key should be overwritten", "value2", jsonObject.getString("key")); } + @Test + public void objectConstructorAppliesExplicitStrictModeToValues() { + JSONParserConfiguration configuration = new JSONParserConfiguration().withStrictMode(); + JSONParserConfiguration lenient = new JSONParserConfiguration().withStrictMode(false); + for (String source : Arrays.asList( + "{\"value\":'text'}", + "{\"value\":[1,]}", + "{\"value\":{\"nested\":1,}}")) { + assertThrows(JSONException.class, () -> new JSONObject(new JSONTokener(source, lenient), configuration)); + } + } + + @Test + public void arrayConstructorAppliesExplicitStrictModeToValues() { + JSONParserConfiguration configuration = new JSONParserConfiguration().withStrictMode(); + JSONParserConfiguration lenient = new JSONParserConfiguration().withStrictMode(false); + for (String source : Arrays.asList( + "['text']", + "[[1,]]", + "[{\"nested\":1,}]")) { + assertThrows(JSONException.class, () -> new JSONArray(new JSONTokener(source, lenient), configuration)); + } + } + + @Test + public void explicitConfigurationOverridesTokenerStrictMode() { + JSONParserConfiguration strict = new JSONParserConfiguration().withStrictMode(); + JSONParserConfiguration lenient = new JSONParserConfiguration().withStrictMode(false); + JSONTokener objectTokener = new JSONTokener("{\"value\":[{'nested':'text',}]}", strict); + JSONTokener arrayTokener = new JSONTokener("[{\"value\":['text',]}]", strict); + + JSONObject object = new JSONObject(objectTokener, lenient); + JSONArray array = new JSONArray(arrayTokener, lenient); + + assertEquals("text", object.getJSONArray("value").getJSONObject(0).getString("nested")); + assertEquals("text", array.getJSONObject(0).getJSONArray("value").getString(0)); + assertSame(lenient, objectTokener.getJsonParserConfiguration()); + assertSame(lenient, arrayTokener.getJsonParserConfiguration()); + } + + @Test + public void explicitConfigurationAppliesToNestedDuplicateKeys() { + JSONParserConfiguration configuration = new JSONParserConfiguration().withOverwriteDuplicateKey(true); + JSONObject object = new JSONObject(new JSONTokener("{\"value\":[{\"key\":1,\"key\":2}]}"), configuration); + JSONArray array = new JSONArray(new JSONTokener("[{\"value\":{\"key\":1,\"key\":2}}]"), configuration); + + assertEquals(2, object.getJSONArray("value").getJSONObject(0).getInt("key")); + assertEquals(2, array.getJSONObject(0).getJSONObject("value").getInt("key")); + } + @Test public void strictModeIsCloned(){ JSONParserConfiguration jsonParserConfiguration = new JSONParserConfiguration() From 9f3b527d9f615205ad12530813d74d368c41c2bf Mon Sep 17 00:00:00 2001 From: sajad mahmoodifar Date: Fri, 18 Sep 2026 16:53:59 +0330 Subject: [PATCH 2/2] fix: restore tokener configuration after parsing --- src/main/java/org/json/JSONArray.java | 61 ++++++++++--------- src/main/java/org/json/JSONObject.java | 25 +++++--- src/main/java/org/json/JSONTokener.java | 4 ++ .../junit/JSONParserConfigurationTest.java | 48 +++++++++++++-- 4 files changed, 94 insertions(+), 44 deletions(-) diff --git a/src/main/java/org/json/JSONArray.java b/src/main/java/org/json/JSONArray.java index a0934e6d9..ff25580e4 100644 --- a/src/main/java/org/json/JSONArray.java +++ b/src/main/java/org/json/JSONArray.java @@ -88,8 +88,9 @@ public JSONArray(JSONTokener x) throws JSONException { /** * Constructs a JSONArray from a JSONTokener and a JSONParserConfiguration. - * The supplied configuration replaces the tokener's configuration and applies - * to all values, including nested objects and arrays. + * The supplied configuration applies to all values during construction, + * including nested objects and arrays. The tokener's original configuration + * is restored afterward, even if parsing fails. * * @param x A JSONTokener instance from which the JSONArray is constructed. * @param jsonParserConfiguration A JSONParserConfiguration instance that controls the behavior of the parser. @@ -110,35 +111,39 @@ public JSONArray(JSONTokener x, JSONParserConfiguration jsonParserConfiguration) */ JSONArray(JSONTokener x, JSONParserConfiguration jsonParserConfiguration, boolean isInitial) throws JSONException { this(); - x.setJsonParserConfiguration(jsonParserConfiguration); - - if (x.nextClean() != '[') { - throw x.syntaxError("A JSONArray text must start with '['"); - } + JSONParserConfiguration originalConfiguration = x.getJsonParserConfiguration(); + x.setJsonParserConfigurationInternal(jsonParserConfiguration); + try { + 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 - throw x.syntaxError("Expected a ',' or ']'"); - } else if (nextChar==',' && jsonParserConfiguration.isStrictMode()) { - throw x.syntaxError("Array content starts with a ','"); - } - if (nextChar != ']') { - x.back(); - for (;;) { - if (x.nextClean() == ',') { - x.back(); - this.myArrayList.add(JSONObject.NULL); - } else { - x.back(); - this.myArrayList.add(x.nextValue()); - } - if (checkForSyntaxError(x, jsonParserConfiguration, isInitial)) return; + char nextChar = x.nextClean(); + if (nextChar == 0) { + // array is unclosed. No ']' found, instead EOF + throw x.syntaxError("Expected a ',' or ']'"); + } else if (nextChar==',' && jsonParserConfiguration.isStrictMode()) { + throw x.syntaxError("Array content starts with a ','"); } - } else { - if (isInitial && jsonParserConfiguration.isStrictMode() && x.nextClean() != 0) { - throw x.syntaxError("Strict mode error: Unparsed characters found at end of input text"); + if (nextChar != ']') { + x.back(); + for (;;) { + if (x.nextClean() == ',') { + x.back(); + this.myArrayList.add(JSONObject.NULL); + } else { + x.back(); + this.myArrayList.add(x.nextValue()); + } + if (checkForSyntaxError(x, jsonParserConfiguration, isInitial)) return; + } + } else { + if (isInitial && jsonParserConfiguration.isStrictMode() && x.nextClean() != 0) { + throw x.syntaxError("Strict mode error: Unparsed characters found at end of input text"); + } } + } finally { + x.setJsonParserConfigurationInternal(originalConfiguration); } } diff --git a/src/main/java/org/json/JSONObject.java b/src/main/java/org/json/JSONObject.java index 77571f73d..3ded23f61 100644 --- a/src/main/java/org/json/JSONObject.java +++ b/src/main/java/org/json/JSONObject.java @@ -204,8 +204,9 @@ public JSONObject(JSONTokener x) throws JSONException { /** * Construct a JSONObject from a JSONTokener with custom json parse configurations. - * The supplied configuration replaces the tokener's configuration and applies - * to all values, including nested objects and arrays. + * The supplied configuration applies to all values during construction, + * including nested objects and arrays. The tokener's original configuration + * is restored afterward, even if parsing fails. * * @param x * A JSONTokener object containing the source string. @@ -235,15 +236,19 @@ public JSONObject(JSONTokener x, JSONParserConfiguration jsonParserConfiguration */ JSONObject(JSONTokener x, JSONParserConfiguration jsonParserConfiguration, boolean isInitial) throws JSONException { this(); - x.setJsonParserConfiguration(jsonParserConfiguration); - - if (x.nextClean() != '{') { - throw x.syntaxError("A JSONObject text must begin with '{'"); - } - for (;;) { - if (parseJSONObject(x, jsonParserConfiguration, isInitial)) { - return; + JSONParserConfiguration originalConfiguration = x.getJsonParserConfiguration(); + x.setJsonParserConfigurationInternal(jsonParserConfiguration); + try { + if (x.nextClean() != '{') { + throw x.syntaxError("A JSONObject text must begin with '{'"); + } + for (;;) { + if (parseJSONObject(x, jsonParserConfiguration, isInitial)) { + return; + } } + } finally { + x.setJsonParserConfigurationInternal(originalConfiguration); } } diff --git a/src/main/java/org/json/JSONTokener.java b/src/main/java/org/json/JSONTokener.java index 60660b020..1acb73bf7 100644 --- a/src/main/java/org/json/JSONTokener.java +++ b/src/main/java/org/json/JSONTokener.java @@ -117,6 +117,10 @@ public JSONParserConfiguration getJsonParserConfiguration() { */ @Deprecated public void setJsonParserConfiguration(JSONParserConfiguration jsonParserConfiguration) { + setJsonParserConfigurationInternal(jsonParserConfiguration); + } + + void setJsonParserConfigurationInternal(JSONParserConfiguration jsonParserConfiguration) { this.jsonParserConfiguration = jsonParserConfiguration; } diff --git a/src/test/java/org/json/junit/JSONParserConfigurationTest.java b/src/test/java/org/json/junit/JSONParserConfigurationTest.java index cbe44474e..21bb84115 100644 --- a/src/test/java/org/json/junit/JSONParserConfigurationTest.java +++ b/src/test/java/org/json/junit/JSONParserConfigurationTest.java @@ -42,10 +42,15 @@ public void objectConstructorAppliesExplicitStrictModeToValues() { JSONParserConfiguration configuration = new JSONParserConfiguration().withStrictMode(); JSONParserConfiguration lenient = new JSONParserConfiguration().withStrictMode(false); for (String source : Arrays.asList( + "[]", + "{", + "{} trailing", "{\"value\":'text'}", "{\"value\":[1,]}", "{\"value\":{\"nested\":1,}}")) { - assertThrows(JSONException.class, () -> new JSONObject(new JSONTokener(source, lenient), configuration)); + JSONTokener tokener = new JSONTokener(source, lenient); + assertThrows(JSONException.class, () -> new JSONObject(tokener, configuration)); + assertSame(lenient, tokener.getJsonParserConfiguration()); } } @@ -54,10 +59,15 @@ public void arrayConstructorAppliesExplicitStrictModeToValues() { JSONParserConfiguration configuration = new JSONParserConfiguration().withStrictMode(); JSONParserConfiguration lenient = new JSONParserConfiguration().withStrictMode(false); for (String source : Arrays.asList( + "{}", + "[", + "[] trailing", "['text']", "[[1,]]", "[{\"nested\":1,}]")) { - assertThrows(JSONException.class, () -> new JSONArray(new JSONTokener(source, lenient), configuration)); + JSONTokener tokener = new JSONTokener(source, lenient); + assertThrows(JSONException.class, () -> new JSONArray(tokener, configuration)); + assertSame(lenient, tokener.getJsonParserConfiguration()); } } @@ -65,16 +75,42 @@ public void arrayConstructorAppliesExplicitStrictModeToValues() { public void explicitConfigurationOverridesTokenerStrictMode() { JSONParserConfiguration strict = new JSONParserConfiguration().withStrictMode(); JSONParserConfiguration lenient = new JSONParserConfiguration().withStrictMode(false); - JSONTokener objectTokener = new JSONTokener("{\"value\":[{'nested':'text',}]}", strict); - JSONTokener arrayTokener = new JSONTokener("[{\"value\":['text',]}]", strict); + JSONTokener objectTokener = new JSONTokener("{\"value\":[{'nested':'text',}], 'other':'text'} 'next'", strict); + JSONTokener arrayTokener = new JSONTokener("[{\"value\":['text',]},'text'] 'next'", strict); JSONObject object = new JSONObject(objectTokener, lenient); JSONArray array = new JSONArray(arrayTokener, lenient); assertEquals("text", object.getJSONArray("value").getJSONObject(0).getString("nested")); assertEquals("text", array.getJSONObject(0).getJSONArray("value").getString(0)); - assertSame(lenient, objectTokener.getJsonParserConfiguration()); - assertSame(lenient, arrayTokener.getJsonParserConfiguration()); + assertEquals("text", object.getString("other")); + assertEquals("text", array.getString(1)); + assertSame(strict, objectTokener.getJsonParserConfiguration()); + assertSame(strict, arrayTokener.getJsonParserConfiguration()); + assertThrows(JSONException.class, () -> objectTokener.nextValue()); + assertThrows(JSONException.class, () -> arrayTokener.nextValue()); + } + + @Test + public void objectConstructorRestoresTokenerConfiguration() { + JSONParserConfiguration original = new JSONParserConfiguration().withStrictMode(false); + JSONParserConfiguration strict = new JSONParserConfiguration().withStrictMode(); + for (String source : Arrays.asList("{}", "{\"value\":[{\"nested\":\"text\"}],\"other\":true}")) { + JSONTokener tokener = new JSONTokener(source, original); + new JSONObject(tokener, strict); + assertSame(original, tokener.getJsonParserConfiguration()); + } + } + + @Test + public void arrayConstructorRestoresTokenerConfiguration() { + JSONParserConfiguration original = new JSONParserConfiguration().withStrictMode(false); + JSONParserConfiguration strict = new JSONParserConfiguration().withStrictMode(); + for (String source : Arrays.asList("[]", "[{\"value\":[\"text\"]},true]")) { + JSONTokener tokener = new JSONTokener(source, original); + new JSONArray(tokener, strict); + assertSame(original, tokener.getJsonParserConfiguration()); + } } @Test