From 21f7a94af13bcd3b20b1f36f49029a10263c446e Mon Sep 17 00:00:00 2001 From: Venkat Date: Sun, 5 Apr 2026 09:12:30 +0530 Subject: [PATCH 1/2] [FEATURE] Global JSONParserConfiguration support --- src/main/java/org/json/JSONArray.java | 8 ++-- src/main/java/org/json/JSONObject.java | 10 ++--- .../org/json/JSONParserConfiguration.java | 41 ++++++++++++++++++- src/main/java/org/json/JSONTokener.java | 4 +- 4 files changed, 51 insertions(+), 12 deletions(-) diff --git a/src/main/java/org/json/JSONArray.java b/src/main/java/org/json/JSONArray.java index 2a3c553a6..fa2c4ae5b 100644 --- a/src/main/java/org/json/JSONArray.java +++ b/src/main/java/org/json/JSONArray.java @@ -184,7 +184,7 @@ private static boolean checkForSyntaxError(JSONTokener x, JSONParserConfiguratio * If there is a syntax error. */ public JSONArray(String source) throws JSONException { - this(source, new JSONParserConfiguration()); + this(source, JSONParserConfiguration.getInstance()); } /** @@ -209,7 +209,7 @@ public JSONArray(String source, JSONParserConfiguration jsonParserConfiguration) * A Collection. */ public JSONArray(Collection collection) { - this(collection, 0, new JSONParserConfiguration()); + this(collection, 0, JSONParserConfiguration.getInstance()); } /** @@ -1410,7 +1410,7 @@ public JSONArray put(int index, long value) throws JSONException { * If a key in the map is null */ public JSONArray put(int index, Map value) throws JSONException { - this.put(index, new JSONObject(value, new JSONParserConfiguration())); + this.put(index, new JSONObject(value, JSONParserConfiguration.getInstance())); return this; } @@ -1967,7 +1967,7 @@ private void addAll(Object array, boolean wrap) throws JSONException { * Variable for tracking the count of nested object creations. */ private void addAll(Object array, boolean wrap, int recursionDepth) { - addAll(array, wrap, recursionDepth, new JSONParserConfiguration()); + addAll(array, wrap, recursionDepth, JSONParserConfiguration.getInstance()); } /** * Add an array's elements to the JSONArray. diff --git a/src/main/java/org/json/JSONObject.java b/src/main/java/org/json/JSONObject.java index 6b087eaba..b01b364aa 100644 --- a/src/main/java/org/json/JSONObject.java +++ b/src/main/java/org/json/JSONObject.java @@ -359,7 +359,7 @@ private static void checkKeyForStrictMode(JSONTokener jsonTokener, JSONParserCon * If a key in the map is null */ public JSONObject(Map m) { - this(m, 0, new JSONParserConfiguration()); + this(m, 0, JSONParserConfiguration.getInstance()); } /** @@ -460,7 +460,7 @@ private JSONObject(Map m, int recursionDepth, JSONParserConfiguration json */ public JSONObject(Object bean) { this(); - this.populateMap(bean, new JSONParserConfiguration()); + this.populateMap(bean, JSONParserConfiguration.getInstance()); } public JSONObject(Object bean, JSONParserConfiguration jsonParserConfiguration) { @@ -470,7 +470,7 @@ public JSONObject(Object bean, JSONParserConfiguration jsonParserConfiguration) private JSONObject(Object bean, Set objectsRecord) { this(); - this.populateMap(bean, objectsRecord, new JSONParserConfiguration()); + this.populateMap(bean, objectsRecord, JSONParserConfiguration.getInstance()); } /** @@ -513,7 +513,7 @@ public JSONObject(Object object, String ... names) { * duplicated key. */ public JSONObject(String source) throws JSONException { - this(source, new JSONParserConfiguration()); + this(source, JSONParserConfiguration.getInstance()); } /** @@ -2960,7 +2960,7 @@ static Object wrap(Object object, int recursionDepth, JSONParserConfiguration js } private static Object wrap(Object object, Set objectsRecord) { - return wrap(object, objectsRecord, 0, new JSONParserConfiguration()); + return wrap(object, objectsRecord, 0, JSONParserConfiguration.getInstance()); } private static Object wrap(Object object, Set objectsRecord, int recursionDepth, JSONParserConfiguration jsonParserConfiguration) { diff --git a/src/main/java/org/json/JSONParserConfiguration.java b/src/main/java/org/json/JSONParserConfiguration.java index 0cfa2eaef..6ae5a7326 100644 --- a/src/main/java/org/json/JSONParserConfiguration.java +++ b/src/main/java/org/json/JSONParserConfiguration.java @@ -4,6 +4,45 @@ * Configuration object for the JSON parser. The configuration is immutable. */ public class JSONParserConfiguration extends ParserConfiguration { + + /** + * system-wide default configuration. + */ + private static JSONParserConfiguration globalConfig = null; + + /** + * Sets the system-wide global JSONParserConfiguration. + * This can only be called once during the application's lifecycle. + * + * @param configuration The configuration to set globally. + * @throws IllegalStateException if the configuration is already set. + * @throws IllegalArgumentException if the provided config is null. + */ + public static void setGlobalConfiguration(JSONParserConfiguration configuration){ + if (configuration == null) { + throw new IllegalArgumentException("Global JSONParserConfiguration cannot be null."); + } + if(globalConfig == null){ + globalConfig = configuration; + } else { + throw new IllegalStateException("Global JSONParserConfiguration has already been set. It cannot be modified."); + } + } + + /** + * Retrieves the system-wide global JSONParserConfiguration. + * If one hasn't been explicitly set, it returns a fresh instance with standard defaults. + * + * @return The active global JSONParserConfiguration. + */ + static JSONParserConfiguration getInstance() { + JSONParserConfiguration config = globalConfig; + if (config == null) { + return new JSONParserConfiguration(); + } + return config; + } + /** * Used to indicate whether to overwrite duplicate key or not. */ @@ -73,7 +112,7 @@ public JSONParserConfiguration withOverwriteDuplicateKey(final boolean overwrite return clone; } - + /** * Controls the parser's behavior when meeting Java null values while converting maps. * If set to true, the parser will put a JSONObject.NULL into the resulting JSONObject. diff --git a/src/main/java/org/json/JSONTokener.java b/src/main/java/org/json/JSONTokener.java index 07ff18c99..cf9e2ceec 100644 --- a/src/main/java/org/json/JSONTokener.java +++ b/src/main/java/org/json/JSONTokener.java @@ -41,7 +41,7 @@ public class JSONTokener { * @param reader the source. */ public JSONTokener(Reader reader) { - this(reader, new JSONParserConfiguration()); + this(reader, JSONParserConfiguration.getInstance()); } /** @@ -70,7 +70,7 @@ public JSONTokener(Reader reader, JSONParserConfiguration jsonParserConfiguratio * @param inputStream The source. */ public JSONTokener(InputStream inputStream) { - this(inputStream, new JSONParserConfiguration()); + this(inputStream, JSONParserConfiguration.getInstance()); } /** From 56dda3cb21c815f1c0ae7d261377fdc95b709bba Mon Sep 17 00:00:00 2001 From: Venkata Krishnan Date: Fri, 24 Apr 2026 13:44:45 +0530 Subject: [PATCH 2/2] [TEST] Global JSONParserConfiguration Testcases --- .../junit/JSONParserConfigurationTest.java | 143 +++++++++++++++++- 1 file changed, 139 insertions(+), 4 deletions(-) diff --git a/src/test/java/org/json/junit/JSONParserConfigurationTest.java b/src/test/java/org/json/junit/JSONParserConfigurationTest.java index 926c49f41..31bc03eda 100644 --- a/src/test/java/org/json/junit/JSONParserConfigurationTest.java +++ b/src/test/java/org/json/junit/JSONParserConfigurationTest.java @@ -8,21 +8,156 @@ import org.junit.Test; import java.io.IOException; +import java.io.StringReader; +import java.lang.reflect.Field; import java.nio.file.Files; import java.nio.file.Paths; import java.util.Arrays; +import java.util.Collections; import java.util.List; +import java.util.Map; import java.util.stream.Collectors; import java.util.stream.Stream; -import static org.junit.Assert.assertEquals; -import static org.junit.Assert.assertThrows; -import static org.junit.Assert.assertTrue; -import static org.junit.Assert.fail; +import org.junit.After; +import org.junit.Before; + +import static org.junit.Assert.*; public class JSONParserConfigurationTest { private static final String TEST_SOURCE = "{\"key\": \"value1\", \"key\": \"value2\"}"; + @Before + public void resetGlobalConfigurationBeforeTest() { + resetGlobalConfiguration(); + } + + @After + public void resetGlobalConfigurationAfterTest() { + resetGlobalConfiguration(); + } + + @Test + public void globalConfigurationShouldRejectNull() { + IllegalArgumentException exception = assertThrows(IllegalArgumentException.class, + () -> JSONParserConfiguration.setGlobalConfiguration(null)); + + assertEquals("Global JSONParserConfiguration cannot be null.", exception.getMessage()); + } + + @Test + public void globalConfigurationShouldOnlyBeSetOnce() { + JSONParserConfiguration.setGlobalConfiguration(new JSONParserConfiguration()); + + IllegalStateException exception = assertThrows(IllegalStateException.class, + () -> JSONParserConfiguration.setGlobalConfiguration(new JSONParserConfiguration().withStrictMode(true))); + + assertEquals("Global JSONParserConfiguration has already been set. It cannot be modified.", + exception.getMessage()); + } + + @Test + public void globalOverwriteDuplicateKeyShouldAffectDefaultJSONObjectStringConstructor() { + JSONParserConfiguration.setGlobalConfiguration(new JSONParserConfiguration().withOverwriteDuplicateKey(true)); + + JSONObject jsonObject = new JSONObject(TEST_SOURCE); + + assertEquals("value2", jsonObject.getString("key")); + } + + @Test + public void globalStrictModeShouldAffectDefaultJSONTokenerReaderConstructor() { + JSONParserConfiguration.setGlobalConfiguration(new JSONParserConfiguration().withStrictMode(true)); + + JSONException exception = assertThrows(JSONException.class, + () -> new JSONObject(new JSONTokener(new StringReader("{\"key\":\"value\"} invalid trailing text")))); + + assertEquals("Strict mode error: Unparsed characters found at end of input text at 17 [character 18 line 1]", + exception.getMessage()); + } + + @Test + public void globalUseNativeNullsShouldAffectDefaultJSONObjectMapConstructor() { + JSONParserConfiguration.setGlobalConfiguration(new JSONParserConfiguration().withUseNativeNulls(true)); + Map nullMap = Collections.singletonMap("nullKey", null); + + JSONObject jsonObject = new JSONObject(nullMap); + + assertTrue(jsonObject.has("nullKey")); + assertEquals(JSONObject.NULL, jsonObject.get("nullKey")); + } + + @Test + public void globalUseNativeNullsShouldAffectDefaultJSONArrayArrayWrapping() { + JSONParserConfiguration.setGlobalConfiguration(new JSONParserConfiguration().withUseNativeNulls(true)); + Map nullMap = Collections.singletonMap("nullKey", null); + + JSONArray jsonArray = new JSONArray(new Object[] { nullMap }); + JSONObject nestedObject = jsonArray.getJSONObject(0); + + assertTrue(nestedObject.has("nullKey")); + assertEquals(JSONObject.NULL, nestedObject.get("nullKey")); + } + + @Test + public void whenNoGlobalConfigurationSet_shouldUseDefaultConfiguration() { + // No global configuration set, should use defaults + // Default: overwriteDuplicateKey=false, so duplicate keys throw exception + JSONException exception = assertThrows(JSONException.class, + () -> new JSONObject(TEST_SOURCE)); + + assertTrue(exception.getMessage().contains("Duplicate key")); + } + + @Test + public void globalStrictModeShouldAffectDefaultJSONArrayStringConstructor() { + JSONParserConfiguration.setGlobalConfiguration(new JSONParserConfiguration().withStrictMode(true)); + + JSONException exception = assertThrows(JSONException.class, + () -> new JSONArray("[\"value\"] invalid trailing text")); + + assertEquals("Strict mode error: Unparsed characters found at end of input text at 11 [character 12 line 1]", + exception.getMessage()); + } + + @Test + public void globalUseNativeNullsShouldAffectDefaultJSONArrayCollectionConstructor() { + JSONParserConfiguration.setGlobalConfiguration(new JSONParserConfiguration().withUseNativeNulls(true)); + Map nullMap = Collections.singletonMap("nullKey", null); + List> collection = Collections.singletonList(nullMap); + + JSONArray jsonArray = new JSONArray(collection); + JSONObject nestedObject = jsonArray.getJSONObject(0); + + assertTrue(nestedObject.has("nullKey")); + assertEquals(JSONObject.NULL, nestedObject.get("nullKey")); + } + + @Test + public void globalMaxNestingDepthShouldAffectDefaultJSONObjectMapConstructor() { + JSONParserConfiguration.setGlobalConfiguration(new JSONParserConfiguration().withMaxNestingDepth(1)); + // Create a nested map that exceeds max depth 1 (depth 0 -> depth 1 -> depth 2 would exceed) + Map innerMostMap = Collections.singletonMap("innermost", "value"); + Map innerMap = Collections.singletonMap("inner", innerMostMap); + Map outerMap = Collections.singletonMap("outer", innerMap); + + JSONException exception = assertThrows(JSONException.class, + () -> new JSONObject(outerMap)); + + assertTrue(exception.getMessage().contains("depth")); + } + + + private void resetGlobalConfiguration() { + try { + Field field = JSONParserConfiguration.class.getDeclaredField("globalConfig"); + field.setAccessible(true); + field.set(null, null); + } catch (ReflectiveOperationException e) { + throw new AssertionError("Unable to reset global JSONParserConfiguration for test isolation.", e); + } + } + @Test(expected = JSONException.class) public void testThrowException() { new JSONObject(TEST_SOURCE);