From 73c582e1295206b85ae1c21af6261f189f19e1c9 Mon Sep 17 00:00:00 2001 From: Simulant87 Date: Fri, 14 Nov 2025 15:29:52 +0100 Subject: [PATCH 01/18] update github actions to version 5 consistently update all actions checkout, setup-java, upload-artifactory to version 5 --- .github/workflows/pipeline.yml | 46 +++++++++++++++++----------------- 1 file changed, 23 insertions(+), 23 deletions(-) diff --git a/.github/workflows/pipeline.yml b/.github/workflows/pipeline.yml index d59702cae..6ada5d597 100644 --- a/.github/workflows/pipeline.yml +++ b/.github/workflows/pipeline.yml @@ -15,9 +15,9 @@ jobs: name: Java 1.6 runs-on: ubuntu-latest steps: - - uses: actions/checkout@v4 + - uses: actions/checkout@v5 - name: Setup java - uses: actions/setup-java@v1 + uses: actions/setup-java@v5 with: java-version: 1.6 - name: Compile Java 1.6 @@ -30,7 +30,7 @@ jobs: jar cvf target/org.json.jar -C target/classes . - name: Upload JAR 1.6 if: ${{ always() }} - uses: actions/upload-artifact@v4 + uses: actions/upload-artifact@v5 with: name: Create java 1.6 JAR path: target/*.jar @@ -45,9 +45,9 @@ jobs: java: [ 8 ] name: Java ${{ matrix.java }} steps: - - uses: actions/checkout@v3 + - uses: actions/checkout@v5 - name: Set up JDK ${{ matrix.java }} - uses: actions/setup-java@v3 + uses: actions/setup-java@v5 with: distribution: 'temurin' java-version: ${{ matrix.java }} @@ -64,13 +64,13 @@ jobs: mvn site -D generateReports=false -D maven.compiler.source=${{ matrix.java }} -D maven.compiler.target=${{ matrix.java }} - name: Upload Test Results ${{ matrix.java }} if: ${{ always() }} - uses: actions/upload-artifact@v4 + uses: actions/upload-artifact@v5 with: name: Test Results ${{ matrix.java }} path: target/surefire-reports/ - name: Upload Test Report ${{ matrix.java }} if: ${{ always() }} - uses: actions/upload-artifact@v4 + uses: actions/upload-artifact@v5 with: name: Test Report ${{ matrix.java }} path: target/site/ @@ -78,7 +78,7 @@ jobs: run: mvn clean package -D maven.compiler.source=${{ matrix.java }} -D maven.compiler.target=${{ matrix.java }} -D maven.test.skip=true -D maven.site.skip=true - name: Upload Package Results ${{ matrix.java }} if: ${{ always() }} - uses: actions/upload-artifact@v4 + uses: actions/upload-artifact@v5 with: name: Package Jar ${{ matrix.java }} path: target/*.jar @@ -93,9 +93,9 @@ jobs: java: [ 11 ] name: Java ${{ matrix.java }} steps: - - uses: actions/checkout@v3 + - uses: actions/checkout@v5 - name: Set up JDK ${{ matrix.java }} - uses: actions/setup-java@v3 + uses: actions/setup-java@v5 with: distribution: 'temurin' java-version: ${{ matrix.java }} @@ -112,13 +112,13 @@ jobs: mvn site -D generateReports=false -D maven.compiler.source=${{ matrix.java }} -D maven.compiler.target=${{ matrix.java }} - name: Upload Test Results ${{ matrix.java }} if: ${{ always() }} - uses: actions/upload-artifact@v4 + uses: actions/upload-artifact@v5 with: name: Test Results ${{ matrix.java }} path: target/surefire-reports/ - name: Upload Test Report ${{ matrix.java }} if: ${{ always() }} - uses: actions/upload-artifact@v4 + uses: actions/upload-artifact@v5 with: name: Test Report ${{ matrix.java }} path: target/site/ @@ -126,7 +126,7 @@ jobs: run: mvn clean package -D maven.compiler.source=${{ matrix.java }} -D maven.compiler.target=${{ matrix.java }} -D maven.test.skip=true -D maven.site.skip=true - name: Upload Package Results ${{ matrix.java }} if: ${{ always() }} - uses: actions/upload-artifact@v4 + uses: actions/upload-artifact@v5 with: name: Package Jar ${{ matrix.java }} path: target/*.jar @@ -141,9 +141,9 @@ jobs: java: [ 17 ] name: Java ${{ matrix.java }} steps: - - uses: actions/checkout@v3 + - uses: actions/checkout@v5 - name: Set up JDK ${{ matrix.java }} - uses: actions/setup-java@v3 + uses: actions/setup-java@v5 with: distribution: 'temurin' java-version: ${{ matrix.java }} @@ -160,13 +160,13 @@ jobs: mvn site -D generateReports=false -D maven.compiler.source=${{ matrix.java }} -D maven.compiler.target=${{ matrix.java }} - name: Upload Test Results ${{ matrix.java }} if: ${{ always() }} - uses: actions/upload-artifact@v4 + uses: actions/upload-artifact@v5 with: name: Test Results ${{ matrix.java }} path: target/surefire-reports/ - name: Upload Test Report ${{ matrix.java }} if: ${{ always() }} - uses: actions/upload-artifact@v4 + uses: actions/upload-artifact@v5 with: name: Test Report ${{ matrix.java }} path: target/site/ @@ -174,7 +174,7 @@ jobs: run: mvn clean package -D maven.compiler.source=${{ matrix.java }} -D maven.compiler.target=${{ matrix.java }} -D maven.test.skip=true -D maven.site.skip=true - name: Upload Package Results ${{ matrix.java }} if: ${{ always() }} - uses: actions/upload-artifact@v4 + uses: actions/upload-artifact@v5 with: name: Package Jar ${{ matrix.java }} path: target/*.jar @@ -189,9 +189,9 @@ jobs: java: [ 21 ] name: Java ${{ matrix.java }} steps: - - uses: actions/checkout@v3 + - uses: actions/checkout@v5 - name: Set up JDK ${{ matrix.java }} - uses: actions/setup-java@v3 + uses: actions/setup-java@v5 with: distribution: 'temurin' java-version: ${{ matrix.java }} @@ -208,13 +208,13 @@ jobs: mvn site -D generateReports=false -D maven.compiler.source=${{ matrix.java }} -D maven.compiler.target=${{ matrix.java }} - name: Upload Test Results ${{ matrix.java }} if: ${{ always() }} - uses: actions/upload-artifact@v4 + uses: actions/upload-artifact@v5 with: name: Test Results ${{ matrix.java }} path: target/surefire-reports/ - name: Upload Test Report ${{ matrix.java }} if: ${{ always() }} - uses: actions/upload-artifact@v4 + uses: actions/upload-artifact@v5 with: name: Test Report ${{ matrix.java }} path: target/site/ @@ -222,7 +222,7 @@ jobs: run: mvn clean package -D maven.compiler.source=${{ matrix.java }} -D maven.compiler.target=${{ matrix.java }} -D maven.test.skip=true -D maven.site.skip=true - name: Upload Package Results ${{ matrix.java }} if: ${{ always() }} - uses: actions/upload-artifact@v4 + uses: actions/upload-artifact@v5 with: name: Package Jar ${{ matrix.java }} path: target/*.jar From e9a7d7c72eeb4a2b48cc51f4798dc3f677936ca1 Mon Sep 17 00:00:00 2001 From: Simulant87 Date: Fri, 14 Nov 2025 15:40:21 +0100 Subject: [PATCH 02/18] add distribution to java 1.6 build --- .github/workflows/pipeline.yml | 1 + 1 file changed, 1 insertion(+) diff --git a/.github/workflows/pipeline.yml b/.github/workflows/pipeline.yml index 6ada5d597..f62ff1fa4 100644 --- a/.github/workflows/pipeline.yml +++ b/.github/workflows/pipeline.yml @@ -20,6 +20,7 @@ jobs: uses: actions/setup-java@v5 with: java-version: 1.6 + distribution: 'temurin' - name: Compile Java 1.6 run: | mkdir -p target/classes From d38cb064fd4ac8a31dde4382343e92a06a246122 Mon Sep 17 00:00:00 2001 From: Simulant87 Date: Fri, 14 Nov 2025 15:45:41 +0100 Subject: [PATCH 03/18] reset setup-java to version 1 for 1.6 build --- .github/workflows/pipeline.yml | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/.github/workflows/pipeline.yml b/.github/workflows/pipeline.yml index f62ff1fa4..e87683ab7 100644 --- a/.github/workflows/pipeline.yml +++ b/.github/workflows/pipeline.yml @@ -17,10 +17,9 @@ jobs: steps: - uses: actions/checkout@v5 - name: Setup java - uses: actions/setup-java@v5 + uses: actions/setup-java@v1 with: java-version: 1.6 - distribution: 'temurin' - name: Compile Java 1.6 run: | mkdir -p target/classes From 005dc7b49eb65a24de0fdfc06757f34e3db8fc72 Mon Sep 17 00:00:00 2001 From: Simulant87 Date: Fri, 14 Nov 2025 15:47:58 +0100 Subject: [PATCH 04/18] add build for LTS JDK 25 --- .github/workflows/pipeline.yml | 49 ++++++++++++++++++++++++++++++++++ 1 file changed, 49 insertions(+) diff --git a/.github/workflows/pipeline.yml b/.github/workflows/pipeline.yml index e87683ab7..85aea5501 100644 --- a/.github/workflows/pipeline.yml +++ b/.github/workflows/pipeline.yml @@ -226,3 +226,52 @@ jobs: with: name: Package Jar ${{ matrix.java }} path: target/*.jar + + build-25: + runs-on: ubuntu-latest + strategy: + fail-fast: false + max-parallel: 1 + matrix: + # build against supported Java LTS versions: + java: [ 25 ] + name: Java ${{ matrix.java }} + steps: + - uses: actions/checkout@v5 + - name: Set up JDK ${{ matrix.java }} + uses: actions/setup-java@v5 + with: + distribution: 'temurin' + java-version: ${{ matrix.java }} + cache: 'maven' + - name: Compile Java ${{ matrix.java }} + run: mvn clean compile -D maven.compiler.source=${{ matrix.java }} -D maven.compiler.target=${{ matrix.java }} -D maven.test.skip=true -D maven.site.skip=true -D maven.javadoc.skip=true + - name: Run Tests ${{ matrix.java }} + run: | + mvn test -D maven.compiler.source=${{ matrix.java }} -D maven.compiler.target=${{ matrix.java }} + - name: Build Test Report ${{ matrix.java }} + if: ${{ always() }} + run: | + mvn surefire-report:report-only -D maven.compiler.source=${{ matrix.java }} -D maven.compiler.target=${{ matrix.java }} + mvn site -D generateReports=false -D maven.compiler.source=${{ matrix.java }} -D maven.compiler.target=${{ matrix.java }} + - name: Upload Test Results ${{ matrix.java }} + if: ${{ always() }} + uses: actions/upload-artifact@v5 + with: + name: Test Results ${{ matrix.java }} + path: target/surefire-reports/ + - name: Upload Test Report ${{ matrix.java }} + if: ${{ always() }} + uses: actions/upload-artifact@v5 + with: + name: Test Report ${{ matrix.java }} + path: target/site/ + - name: Package Jar ${{ matrix.java }} + run: mvn clean package -D maven.compiler.source=${{ matrix.java }} -D maven.compiler.target=${{ matrix.java }} -D maven.test.skip=true -D maven.site.skip=true + - name: Upload Package Results ${{ matrix.java }} + if: ${{ always() }} + uses: actions/upload-artifact@v5 + with: + name: Package Jar ${{ matrix.java }} + path: target/*.jar + From 3bc98dfc7fccd1459eba20b1c4e5561d8dfca78d Mon Sep 17 00:00:00 2001 From: Simulant87 Date: Fri, 14 Nov 2025 15:49:09 +0100 Subject: [PATCH 05/18] Update README.md tested on java 25 --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index 28f71971e..994e7f675 100644 --- a/README.md +++ b/README.md @@ -26,7 +26,7 @@ Project goals include: * No external dependencies * Fast execution and low memory footprint * Maintain backward compatibility -* Designed and tested to use on Java versions 1.6 - 21 +* Designed and tested to use on Java versions 1.6 - 25 The files in this package implement JSON encoders and decoders. The package can also convert between JSON and XML, HTTP headers, Cookies, and CDL. From 421abfdc1f6e7a70a71b0d436f9f8e50ddae29e4 Mon Sep 17 00:00:00 2001 From: Simulant Date: Sat, 20 Dec 2025 22:27:45 +0100 Subject: [PATCH 06/18] save and restore the current default locale, to avoid any side effects on other executions in the same JVM --- .../org/json/junit/JSONObjectLocaleTest.java | 46 +++++++++++-------- 1 file changed, 26 insertions(+), 20 deletions(-) diff --git a/src/test/java/org/json/junit/JSONObjectLocaleTest.java b/src/test/java/org/json/junit/JSONObjectLocaleTest.java index 1cdaf743d..e1a9dd64e 100755 --- a/src/test/java/org/json/junit/JSONObjectLocaleTest.java +++ b/src/test/java/org/json/junit/JSONObjectLocaleTest.java @@ -36,25 +36,31 @@ public void jsonObjectByLocaleBean() { MyLocaleBean myLocaleBean = new MyLocaleBean(); - /** - * This is just the control case which happens when the locale.ROOT - * lowercasing behavior is the same as the current locale. - */ - Locale.setDefault(new Locale("en")); - JSONObject jsonen = new JSONObject(myLocaleBean); - assertEquals("expected size 2, found: " +jsonen.length(), 2, jsonen.length()); - assertEquals("expected jsonen[i] == beanI", "beanI", jsonen.getString("i")); - assertEquals("expected jsonen[id] == beanId", "beanId", jsonen.getString("id")); - - /** - * Without the JSON-Java change, these keys would be stored internally as - * starting with the letter, 'ı' (dotless i), since the lowercasing of - * the getI and getId keys would be specific to the Turkish locale. - */ - Locale.setDefault(new Locale("tr")); - JSONObject jsontr = new JSONObject(myLocaleBean); - assertEquals("expected size 2, found: " +jsontr.length(), 2, jsontr.length()); - assertEquals("expected jsontr[i] == beanI", "beanI", jsontr.getString("i")); - assertEquals("expected jsontr[id] == beanId", "beanId", jsontr.getString("id")); + // save and restore the current default locale, to avoid any side effects on other executions in the same JVM + Locale defaultLocale = Locale.getDefault(); + try { + /** + * This is just the control case which happens when the locale.ROOT + * lowercasing behavior is the same as the current locale. + */ + Locale.setDefault(new Locale("en")); + JSONObject jsonen = new JSONObject(myLocaleBean); + assertEquals("expected size 2, found: " +jsonen.length(), 2, jsonen.length()); + assertEquals("expected jsonen[i] == beanI", "beanI", jsonen.getString("i")); + assertEquals("expected jsonen[id] == beanId", "beanId", jsonen.getString("id")); + + /** + * Without the JSON-Java change, these keys would be stored internally as + * starting with the letter, 'ı' (dotless i), since the lowercasing of + * the getI and getId keys would be specific to the Turkish locale. + */ + Locale.setDefault(new Locale("tr")); + JSONObject jsontr = new JSONObject(myLocaleBean); + assertEquals("expected size 2, found: " +jsontr.length(), 2, jsontr.length()); + assertEquals("expected jsontr[i] == beanI", "beanI", jsontr.getString("i")); + assertEquals("expected jsontr[id] == beanId", "beanId", jsontr.getString("id")); + } finally { + Locale.setDefault(defaultLocale); + } } } From 8cbb4d5bb3c2e27a13bb6229cce19441d9092860 Mon Sep 17 00:00:00 2001 From: Simulant Date: Sat, 20 Dec 2025 22:57:24 +0100 Subject: [PATCH 07/18] Fix sonarqube reliability issues --- src/main/java/org/json/XML.java | 6 +++++- .../java/org/json/junit/JSONObjectTest.java | 20 ++++++++++--------- 2 files changed, 16 insertions(+), 10 deletions(-) diff --git a/src/main/java/org/json/XML.java b/src/main/java/org/json/XML.java index 3eb948c77..e14bb34e9 100644 --- a/src/main/java/org/json/XML.java +++ b/src/main/java/org/json/XML.java @@ -9,6 +9,7 @@ import java.math.BigDecimal; import java.math.BigInteger; import java.util.Iterator; +import java.util.NoSuchElementException; /** * This provides static methods to convert an XML text into a JSONObject, and to @@ -80,7 +81,7 @@ private static Iterable codePointIterator(final String string) { public Iterator iterator() { return new Iterator() { private int nextIndex = 0; - private int length = string.length(); + private final int length = string.length(); @Override public boolean hasNext() { @@ -89,6 +90,9 @@ public boolean hasNext() { @Override public Integer next() { + if (!hasNext()) { + throw new NoSuchElementException(); + } int result = string.codePointAt(this.nextIndex); this.nextIndex += Character.charCount(result); return result; diff --git a/src/test/java/org/json/junit/JSONObjectTest.java b/src/test/java/org/json/junit/JSONObjectTest.java index 7ca6093b7..5c1d1a2eb 100644 --- a/src/test/java/org/json/junit/JSONObjectTest.java +++ b/src/test/java/org/json/junit/JSONObjectTest.java @@ -3117,12 +3117,13 @@ public void testJSONWriterException() { // test a more complex object writer = new StringWriter(); - try { - new JSONObject() + + JSONObject object = new JSONObject() .put("somethingElse", "a value") .put("someKey", new JSONArray() - .put(new JSONObject().put("key1", new BrokenToString()))) - .write(writer).toString(); + .put(new JSONObject().put("key1", new BrokenToString()))); + try { + object.write(writer).toString(); fail("Expected an exception, got a String value"); } catch (JSONException e) { assertEquals("Unable to write JSONObject value for key: someKey", e.getMessage()); @@ -3133,17 +3134,18 @@ public void testJSONWriterException() { writer.close(); } catch (Exception e) {} } - + // test a more slightly complex object writer = new StringWriter(); - try { - new JSONObject() + + object = new JSONObject() .put("somethingElse", "a value") .put("someKey", new JSONArray() .put(new JSONObject().put("key1", new BrokenToString())) .put(12345) - ) - .write(writer).toString(); + ); + try { + object.write(writer).toString(); fail("Expected an exception, got a String value"); } catch (JSONException e) { assertEquals("Unable to write JSONObject value for key: someKey", e.getMessage()); From 96353de30481beab97895e309c0d9d4a1a8d9167 Mon Sep 17 00:00:00 2001 From: Simulant Date: Sun, 21 Dec 2025 23:16:01 +0100 Subject: [PATCH 08/18] add badge to external hosted javadoc --- README.md | 1 + 1 file changed, 1 insertion(+) diff --git a/README.md b/README.md index 994e7f675..1a59b91a7 100644 --- a/README.md +++ b/README.md @@ -9,6 +9,7 @@ JSON in Java [package org.json] [![Maven Central](https://img.shields.io/maven-central/v/org.json/json.svg)](https://mvnrepository.com/artifact/org.json/json) [![Java CI with Maven](https://github.com/stleary/JSON-java/actions/workflows/pipeline.yml/badge.svg)](https://github.com/stleary/JSON-java/actions/workflows/pipeline.yml) [![CodeQL](https://github.com/stleary/JSON-java/actions/workflows/codeql-analysis.yml/badge.svg)](https://github.com/stleary/JSON-java/actions/workflows/codeql-analysis.yml) +[![javadoc](https://javadoc.io/badge2/org.json/json/javadoc.svg)](https://javadoc.io/doc/org.json/json) **[Click here if you just want the latest release jar file.](https://search.maven.org/remotecontent?filepath=org/json/json/20250517/json-20250517.jar)** From 24bba97c1d21fdb9bab76940503be7579d874476 Mon Sep 17 00:00:00 2001 From: Sean Leary Date: Wed, 24 Dec 2025 09:05:18 -0600 Subject: [PATCH 09/18] pre-release-20251224 update docs and builds for next release --- README.md | 2 +- build.gradle | 2 +- docs/RELEASES.md | 2 ++ pom.xml | 2 +- 4 files changed, 5 insertions(+), 3 deletions(-) diff --git a/README.md b/README.md index 994e7f675..e341a0b34 100644 --- a/README.md +++ b/README.md @@ -10,7 +10,7 @@ JSON in Java [package org.json] [![Java CI with Maven](https://github.com/stleary/JSON-java/actions/workflows/pipeline.yml/badge.svg)](https://github.com/stleary/JSON-java/actions/workflows/pipeline.yml) [![CodeQL](https://github.com/stleary/JSON-java/actions/workflows/codeql-analysis.yml/badge.svg)](https://github.com/stleary/JSON-java/actions/workflows/codeql-analysis.yml) -**[Click here if you just want the latest release jar file.](https://search.maven.org/remotecontent?filepath=org/json/json/20250517/json-20250517.jar)** +**[Click here if you just want the latest release jar file.](https://search.maven.org/remotecontent?filepath=org/json/json/20251224/json-20251224.jar)** # Overview diff --git a/build.gradle b/build.gradle index 6dcdca6fc..898f10dc7 100644 --- a/build.gradle +++ b/build.gradle @@ -42,7 +42,7 @@ subprojects { } group = 'org.json' -version = 'v20250517-SNAPSHOT' +version = 'v20251224-SNAPSHOT' description = 'JSON in Java' sourceCompatibility = '1.8' diff --git a/docs/RELEASES.md b/docs/RELEASES.md index cd53bbe55..653e2bb8c 100644 --- a/docs/RELEASES.md +++ b/docs/RELEASES.md @@ -5,6 +5,8 @@ and artifactId "json". For example: [https://search.maven.org/search?q=g:org.json%20AND%20a:json&core=gav](https://search.maven.org/search?q=g:org.json%20AND%20a:json&core=gav) ~~~ +20251224 Records, fromJson(), and recent commits + 20250517 Strict mode hardening and recent commits 20250107 Restore moditect in pom.xml diff --git a/pom.xml b/pom.xml index 81f5c3c2c..8d0881cbe 100644 --- a/pom.xml +++ b/pom.xml @@ -3,7 +3,7 @@ org.json json - 20250517 + 20251224 bundle JSON in Java From 995fb840f79bf8b9230a08e45cd2289fb21b3cc6 Mon Sep 17 00:00:00 2001 From: Pratik Tiwari Date: Fri, 2 Jan 2026 21:20:53 +0530 Subject: [PATCH 10/18] Fixes the issue of losing the array if an empty forceList element or a tag is in the middle or the end --- src/main/java/org/json/XML.java | 7 +- .../org/json/junit/XMLConfigurationTest.java | 108 ++++++++++++++++++ 2 files changed, 113 insertions(+), 2 deletions(-) diff --git a/src/main/java/org/json/XML.java b/src/main/java/org/json/XML.java index e14bb34e9..d0216f5d8 100644 --- a/src/main/java/org/json/XML.java +++ b/src/main/java/org/json/XML.java @@ -391,7 +391,7 @@ private static boolean parse(XMLTokener x, JSONObject context, String name, XMLP context.append(tagName, JSONObject.NULL); } else if (jsonObject.length() > 0) { context.append(tagName, jsonObject); - } else { + } else if(context.isEmpty() && (context.opt(tagName) == null || !(context.get(tagName) instanceof JSONArray))) { //avoids resetting the array in case of an empty tag in the middle or end context.put(tagName, new JSONArray()); } } else { @@ -451,7 +451,10 @@ private static boolean parse(XMLTokener x, JSONObject context, String name, XMLP if (config.getForceList().contains(tagName)) { // Force the value to be an array if (jsonObject.length() == 0) { - context.put(tagName, new JSONArray()); + //avoids resetting the array in case of an empty element in the middle or end + if(context.length()==0 && context.opt(tagName) == null || !(context.get(tagName) instanceof JSONArray)) { + context.put(tagName, new JSONArray()); + } } else if (jsonObject.length() == 1 && jsonObject.opt(config.getcDataTagName()) != null) { context.append(tagName, jsonObject.opt(config.getcDataTagName())); diff --git a/src/test/java/org/json/junit/XMLConfigurationTest.java b/src/test/java/org/json/junit/XMLConfigurationTest.java index ca1980c8a..8edbe7984 100755 --- a/src/test/java/org/json/junit/XMLConfigurationTest.java +++ b/src/test/java/org/json/junit/XMLConfigurationTest.java @@ -1144,6 +1144,114 @@ public void testEmptyTagForceList() { Util.compareActualVsExpectedJsonObjects(jsonObject, expetedJsonObject); } + @Test + public void testForceListWithLastElementAsEmptyTag(){ + final String originalXml = "1"; + final String expectedJsonString = "{\"root\":{\"id\":[1]}}"; + + HashSet forceListCandidates = new HashSet<>(); + forceListCandidates.add("id"); + final JSONObject json = XML.toJSONObject(originalXml, + new XMLParserConfiguration() + .withKeepStrings(false) + .withcDataTagName("content") + .withForceList(forceListCandidates) + .withConvertNilAttributeToNull(true)); + assertEquals(expectedJsonString, json.toString()); + } + + @Test + public void testForceListWithFirstElementAsEmptyTag(){ + final String originalXml = "1"; + final String expectedJsonString = "{\"root\":{\"id\":[1]}}"; + + HashSet forceListCandidates = new HashSet<>(); + forceListCandidates.add("id"); + final JSONObject json = XML.toJSONObject(originalXml, + new XMLParserConfiguration() + .withKeepStrings(false) + .withcDataTagName("content") + .withForceList(forceListCandidates) + .withConvertNilAttributeToNull(true)); + assertEquals(expectedJsonString, json.toString()); + } + + @Test + public void testForceListWithMiddleElementAsEmptyTag(){ + final String originalXml = "12"; + final String expectedJsonString = "{\"root\":{\"id\":[1,2]}}"; + + HashSet forceListCandidates = new HashSet<>(); + forceListCandidates.add("id"); + final JSONObject json = XML.toJSONObject(originalXml, + new XMLParserConfiguration() + .withKeepStrings(false) + .withcDataTagName("content") + .withForceList(forceListCandidates) + .withConvertNilAttributeToNull(true)); + assertEquals(expectedJsonString, json.toString()); + } + + @Test + public void testForceListWithLastElementAsEmpty(){ + final String originalXml = "1"; + final String expectedJsonString = "{\"root\":{\"id\":[1]}}"; + + HashSet forceListCandidates = new HashSet<>(); + forceListCandidates.add("id"); + final JSONObject json = XML.toJSONObject(originalXml, + new XMLParserConfiguration() + .withKeepStrings(false) + .withForceList(forceListCandidates) + .withConvertNilAttributeToNull(true)); + assertEquals(expectedJsonString, json.toString()); + } + + @Test + public void testForceListWithFirstElementAsEmpty(){ + final String originalXml = "1"; + final String expectedJsonString = "{\"root\":{\"id\":[1]}}"; + + HashSet forceListCandidates = new HashSet<>(); + forceListCandidates.add("id"); + final JSONObject json = XML.toJSONObject(originalXml, + new XMLParserConfiguration() + .withKeepStrings(false) + .withForceList(forceListCandidates) + .withConvertNilAttributeToNull(true)); + assertEquals(expectedJsonString, json.toString()); + } + + @Test + public void testForceListWithMiddleElementAsEmpty(){ + final String originalXml = "12"; + final String expectedJsonString = "{\"root\":{\"id\":[1,2]}}"; + + HashSet forceListCandidates = new HashSet<>(); + forceListCandidates.add("id"); + final JSONObject json = XML.toJSONObject(originalXml, + new XMLParserConfiguration() + .withKeepStrings(false) + .withForceList(forceListCandidates) + .withConvertNilAttributeToNull(true)); + assertEquals(expectedJsonString, json.toString()); + } + + @Test + public void testForceListEmptyAndEmptyTagsMixed(){ + final String originalXml = "12"; + final String expectedJsonString = "{\"root\":{\"id\":[1,2]}}"; + + HashSet forceListCandidates = new HashSet<>(); + forceListCandidates.add("id"); + final JSONObject json = XML.toJSONObject(originalXml, + new XMLParserConfiguration() + .withKeepStrings(false) + .withForceList(forceListCandidates) + .withConvertNilAttributeToNull(true)); + assertEquals(expectedJsonString, json.toString()); + } + @Test public void testMaxNestingDepthIsSet() { XMLParserConfiguration xmlParserConfiguration = XMLParserConfiguration.ORIGINAL; From 9d14246bee04fae8f7199d281671773c9c11537d Mon Sep 17 00:00:00 2001 From: OwenSanzas Date: Tue, 27 Jan 2026 11:36:46 +0000 Subject: [PATCH 11/18] Fix ClassCastException in JSONML.toJSONArray and toJSONObject Add type checking before casting parse() results to JSONArray/JSONObject. When parse() returns an unexpected type (e.g., String for malformed input), the code now throws a descriptive JSONException instead of ClassCastException. This prevents unchecked exceptions from propagating to callers who only expect JSONException from these methods. Fixes #1034 --- src/main/java/org/json/JSONML.java | 51 +++++++++++++++++++++++------- 1 file changed, 39 insertions(+), 12 deletions(-) diff --git a/src/main/java/org/json/JSONML.java b/src/main/java/org/json/JSONML.java index bde97a680..6ec997061 100644 --- a/src/main/java/org/json/JSONML.java +++ b/src/main/java/org/json/JSONML.java @@ -22,6 +22,33 @@ public class JSONML { public JSONML() { } + /** + * Safely cast parse result to JSONArray with proper type checking. + * @param result The result from parse() method + * @return JSONArray if result is a JSONArray + * @throws JSONException if result is not a JSONArray + */ + private static JSONArray toJSONArraySafe(Object result) throws JSONException { + if (result instanceof JSONArray) { + return (JSONArray) result; + } + throw new JSONException("Expected JSONArray but got " + + (result == null ? "null" : result.getClass().getSimpleName())); + } + + /** + * Safely cast parse result to JSONObject with proper type checking. + * @param result The result from parse() method + * @return JSONObject if result is a JSONObject + * @throws JSONException if result is not a JSONObject + */ + private static JSONObject toJSONObjectSafe(Object result) throws JSONException { + if (result instanceof JSONObject) { + return (JSONObject) result; + } + throw new JSONException("Expected JSONObject but got " + + (result == null ? "null" : result.getClass().getSimpleName())); + } /** * Parse XML values and store them in a JSONArray. @@ -276,7 +303,7 @@ private static Object parse( * @throws JSONException Thrown on error converting to a JSONArray */ public static JSONArray toJSONArray(String string) throws JSONException { - return (JSONArray)parse(new XMLTokener(string), true, null, JSONMLParserConfiguration.ORIGINAL, 0); + return toJSONArraySafe(parse(new XMLTokener(string), true, null, JSONMLParserConfiguration.ORIGINAL, 0)); } @@ -298,7 +325,7 @@ public static JSONArray toJSONArray(String string) throws JSONException { * @throws JSONException Thrown on error converting to a JSONArray */ public static JSONArray toJSONArray(String string, boolean keepStrings) throws JSONException { - return (JSONArray)parse(new XMLTokener(string), true, null, keepStrings, 0); + return toJSONArraySafe(parse(new XMLTokener(string), true, null, keepStrings, 0)); } @@ -323,7 +350,7 @@ public static JSONArray toJSONArray(String string, boolean keepStrings) throws J * @throws JSONException Thrown on error converting to a JSONArray */ public static JSONArray toJSONArray(String string, JSONMLParserConfiguration config) throws JSONException { - return (JSONArray)parse(new XMLTokener(string), true, null, config, 0); + return toJSONArraySafe(parse(new XMLTokener(string), true, null, config, 0)); } @@ -347,7 +374,7 @@ public static JSONArray toJSONArray(String string, JSONMLParserConfiguration con * @throws JSONException Thrown on error converting to a JSONArray */ public static JSONArray toJSONArray(XMLTokener x, JSONMLParserConfiguration config) throws JSONException { - return (JSONArray)parse(x, true, null, config, 0); + return toJSONArraySafe(parse(x, true, null, config, 0)); } @@ -369,7 +396,7 @@ public static JSONArray toJSONArray(XMLTokener x, JSONMLParserConfiguration conf * @throws JSONException Thrown on error converting to a JSONArray */ public static JSONArray toJSONArray(XMLTokener x, boolean keepStrings) throws JSONException { - return (JSONArray)parse(x, true, null, keepStrings, 0); + return toJSONArraySafe(parse(x, true, null, keepStrings, 0)); } @@ -386,7 +413,7 @@ public static JSONArray toJSONArray(XMLTokener x, boolean keepStrings) throws JS * @throws JSONException Thrown on error converting to a JSONArray */ public static JSONArray toJSONArray(XMLTokener x) throws JSONException { - return (JSONArray)parse(x, true, null, false, 0); + return toJSONArraySafe(parse(x, true, null, false, 0)); } @@ -404,7 +431,7 @@ public static JSONArray toJSONArray(XMLTokener x) throws JSONException { * @throws JSONException Thrown on error converting to a JSONObject */ public static JSONObject toJSONObject(String string) throws JSONException { - return (JSONObject)parse(new XMLTokener(string), false, null, false, 0); + return toJSONObjectSafe(parse(new XMLTokener(string), false, null, false, 0)); } @@ -424,7 +451,7 @@ public static JSONObject toJSONObject(String string) throws JSONException { * @throws JSONException Thrown on error converting to a JSONObject */ public static JSONObject toJSONObject(String string, boolean keepStrings) throws JSONException { - return (JSONObject)parse(new XMLTokener(string), false, null, keepStrings, 0); + return toJSONObjectSafe(parse(new XMLTokener(string), false, null, keepStrings, 0)); } @@ -446,7 +473,7 @@ public static JSONObject toJSONObject(String string, boolean keepStrings) throws * @throws JSONException Thrown on error converting to a JSONObject */ public static JSONObject toJSONObject(String string, JSONMLParserConfiguration config) throws JSONException { - return (JSONObject)parse(new XMLTokener(string), false, null, config, 0); + return toJSONObjectSafe(parse(new XMLTokener(string), false, null, config, 0)); } @@ -464,7 +491,7 @@ public static JSONObject toJSONObject(String string, JSONMLParserConfiguration c * @throws JSONException Thrown on error converting to a JSONObject */ public static JSONObject toJSONObject(XMLTokener x) throws JSONException { - return (JSONObject)parse(x, false, null, false, 0); + return toJSONObjectSafe(parse(x, false, null, false, 0)); } @@ -484,7 +511,7 @@ public static JSONObject toJSONObject(XMLTokener x) throws JSONException { * @throws JSONException Thrown on error converting to a JSONObject */ public static JSONObject toJSONObject(XMLTokener x, boolean keepStrings) throws JSONException { - return (JSONObject)parse(x, false, null, keepStrings, 0); + return toJSONObjectSafe(parse(x, false, null, keepStrings, 0)); } @@ -506,7 +533,7 @@ public static JSONObject toJSONObject(XMLTokener x, boolean keepStrings) throws * @throws JSONException Thrown on error converting to a JSONObject */ public static JSONObject toJSONObject(XMLTokener x, JSONMLParserConfiguration config) throws JSONException { - return (JSONObject)parse(x, false, null, config, 0); + return toJSONObjectSafe(parse(x, false, null, config, 0)); } From 534ce3c4d1a2024dda21e3e6411a717896d455fe Mon Sep 17 00:00:00 2001 From: OwenSanzas Date: Tue, 27 Jan 2026 11:40:18 +0000 Subject: [PATCH 12/18] Fix input validation in XMLTokener.unescapeEntity() Fix StringIndexOutOfBoundsException and NumberFormatException in XMLTokener.unescapeEntity() when parsing malformed XML numeric character references. Issues: - &#; (empty numeric reference) caused StringIndexOutOfBoundsException - &#txx; (invalid decimal) caused NumberFormatException - &#xGGG; (invalid hex) caused NumberFormatException Changes: - Add length validation before accessing character positions - Add isValidHex() and isValidDecimal() helper methods - Throw proper JSONException with descriptive messages Fixes #1035, Fixes #1036 --- src/main/java/org/json/XMLTokener.java | 76 +++++++++++++++++++++++--- 1 file changed, 68 insertions(+), 8 deletions(-) diff --git a/src/main/java/org/json/XMLTokener.java b/src/main/java/org/json/XMLTokener.java index bc18b31c9..922589dec 100644 --- a/src/main/java/org/json/XMLTokener.java +++ b/src/main/java/org/json/XMLTokener.java @@ -151,9 +151,10 @@ public Object nextEntity(@SuppressWarnings("unused") char ampersand) throws JSON /** * Unescape an XML entity encoding; * @param e entity (only the actual entity value, not the preceding & or ending ; - * @return + * @return the unescaped entity string + * @throws JSONException if the entity is malformed */ - static String unescapeEntity(String e) { + static String unescapeEntity(String e) throws JSONException { // validate if (e == null || e.isEmpty()) { return ""; @@ -161,23 +162,82 @@ static String unescapeEntity(String e) { // if our entity is an encoded unicode point, parse it. if (e.charAt(0) == '#') { int cp; + // Check minimum length for numeric character reference + if (e.length() < 2) { + throw new JSONException("Invalid numeric character reference: &#;"); + } if (e.charAt(1) == 'x' || e.charAt(1) == 'X') { - // hex encoded unicode - cp = Integer.parseInt(e.substring(2), 16); + // hex encoded unicode - need at least one hex digit after #x + if (e.length() < 3) { + throw new JSONException("Invalid hex character reference: missing hex digits in &#" + e.substring(1) + ";"); + } + String hex = e.substring(2); + if (!isValidHex(hex)) { + throw new JSONException("Invalid hex character reference: &#" + e.substring(1) + ";"); + } + try { + cp = Integer.parseInt(hex, 16); + } catch (NumberFormatException nfe) { + throw new JSONException("Invalid hex character reference: &#" + e.substring(1) + ";", nfe); + } } else { // decimal encoded unicode - cp = Integer.parseInt(e.substring(1)); + String decimal = e.substring(1); + if (!isValidDecimal(decimal)) { + throw new JSONException("Invalid decimal character reference: &#" + decimal + ";"); + } + try { + cp = Integer.parseInt(decimal); + } catch (NumberFormatException nfe) { + throw new JSONException("Invalid decimal character reference: &#" + decimal + ";", nfe); + } } - return new String(new int[] {cp},0,1); - } + return new String(new int[] {cp}, 0, 1); + } Character knownEntity = entity.get(e); - if(knownEntity==null) { + if (knownEntity == null) { // we don't know the entity so keep it encoded return '&' + e + ';'; } return knownEntity.toString(); } + /** + * Check if a string contains only valid hexadecimal digits. + * @param s the string to check + * @return true if s is non-empty and contains only hex digits (0-9, a-f, A-F) + */ + private static boolean isValidHex(String s) { + if (s == null || s.isEmpty()) { + return false; + } + for (int i = 0; i < s.length(); i++) { + char c = s.charAt(i); + if (!((c >= '0' && c <= '9') || (c >= 'a' && c <= 'f') || (c >= 'A' && c <= 'F'))) { + return false; + } + } + return true; + } + + /** + * Check if a string contains only valid decimal digits. + * @param s the string to check + * @return true if s is non-empty and contains only digits (0-9) + */ + private static boolean isValidDecimal(String s) { + if (s == null || s.isEmpty()) { + return false; + } + for (int i = 0; i < s.length(); i++) { + char c = s.charAt(i); + if (c < '0' || c > '9') { + return false; + } + } + return true; + } + /** *
{@code 

From 6c1bfbc7a58185ba915f804c0aa6e00ae4fb621b Mon Sep 17 00:00:00 2001
From: OwenSanzas 
Date: Wed, 28 Jan 2026 09:52:25 +0000
Subject: [PATCH 13/18] Refactor XMLTokener.unescapeEntity() to reduce
 complexity

Extracted hex and decimal parsing logic into separate methods to
address SonarQube complexity warning:
- parseHexEntity(): handles ઼ format
- parseDecimalEntity(): handles { format

This reduces cyclomatic complexity while maintaining identical
functionality and all validation checks.
---
 .gitignore                             |  3 ++
 src/main/java/org/json/XMLTokener.java | 71 ++++++++++++++++----------
 2 files changed, 46 insertions(+), 28 deletions(-)

diff --git a/.gitignore b/.gitignore
index b78af4db7..0e08d645c 100644
--- a/.gitignore
+++ b/.gitignore
@@ -16,3 +16,6 @@ build
 /gradlew
 /gradlew.bat
 .gitmodules
+
+# ignore compiled class files
+*.class
diff --git a/src/main/java/org/json/XMLTokener.java b/src/main/java/org/json/XMLTokener.java
index 922589dec..dad2e2897 100644
--- a/src/main/java/org/json/XMLTokener.java
+++ b/src/main/java/org/json/XMLTokener.java
@@ -161,37 +161,12 @@ static String unescapeEntity(String e) throws JSONException {
         }
         // if our entity is an encoded unicode point, parse it.
         if (e.charAt(0) == '#') {
-            int cp;
-            // Check minimum length for numeric character reference
             if (e.length() < 2) {
                 throw new JSONException("Invalid numeric character reference: &#;");
             }
-            if (e.charAt(1) == 'x' || e.charAt(1) == 'X') {
-                // hex encoded unicode - need at least one hex digit after #x
-                if (e.length() < 3) {
-                    throw new JSONException("Invalid hex character reference: missing hex digits in &#" + e.substring(1) + ";");
-                }
-                String hex = e.substring(2);
-                if (!isValidHex(hex)) {
-                    throw new JSONException("Invalid hex character reference: &#" + e.substring(1) + ";");
-                }
-                try {
-                    cp = Integer.parseInt(hex, 16);
-                } catch (NumberFormatException nfe) {
-                    throw new JSONException("Invalid hex character reference: &#" + e.substring(1) + ";", nfe);
-                }
-            } else {
-                // decimal encoded unicode
-                String decimal = e.substring(1);
-                if (!isValidDecimal(decimal)) {
-                    throw new JSONException("Invalid decimal character reference: &#" + decimal + ";");
-                }
-                try {
-                    cp = Integer.parseInt(decimal);
-                } catch (NumberFormatException nfe) {
-                    throw new JSONException("Invalid decimal character reference: &#" + decimal + ";", nfe);
-                }
-            }
+            int cp = (e.charAt(1) == 'x' || e.charAt(1) == 'X')
+                ? parseHexEntity(e)
+                : parseDecimalEntity(e);
             return new String(new int[] {cp}, 0, 1);
         }
         Character knownEntity = entity.get(e);
@@ -202,6 +177,46 @@ static String unescapeEntity(String e) throws JSONException {
         return knownEntity.toString();
     }
 
+    /**
+     * Parse a hexadecimal numeric character reference (e.g., "઼").
+     * @param e entity string starting with '#' (e.g., "#x1F4A9")
+     * @return the Unicode code point
+     * @throws JSONException if the format is invalid
+     */
+    private static int parseHexEntity(String e) throws JSONException {
+        // hex encoded unicode - need at least one hex digit after #x
+        if (e.length() < 3) {
+            throw new JSONException("Invalid hex character reference: missing hex digits in &#" + e.substring(1) + ";");
+        }
+        String hex = e.substring(2);
+        if (!isValidHex(hex)) {
+            throw new JSONException("Invalid hex character reference: &#" + e.substring(1) + ";");
+        }
+        try {
+            return Integer.parseInt(hex, 16);
+        } catch (NumberFormatException nfe) {
+            throw new JSONException("Invalid hex character reference: &#" + e.substring(1) + ";", nfe);
+        }
+    }
+
+    /**
+     * Parse a decimal numeric character reference (e.g., "{").
+     * @param e entity string starting with '#' (e.g., "#123")
+     * @return the Unicode code point
+     * @throws JSONException if the format is invalid
+     */
+    private static int parseDecimalEntity(String e) throws JSONException {
+        String decimal = e.substring(1);
+        if (!isValidDecimal(decimal)) {
+            throw new JSONException("Invalid decimal character reference: &#" + decimal + ";");
+        }
+        try {
+            return Integer.parseInt(decimal);
+        } catch (NumberFormatException nfe) {
+            throw new JSONException("Invalid decimal character reference: &#" + decimal + ";", nfe);
+        }
+    }
+
     /**
      * Check if a string contains only valid hexadecimal digits.
      * @param s the string to check

From 592e7828d9729c35053a595134e137098a053177 Mon Sep 17 00:00:00 2001
From: OwenSanzas 
Date: Wed, 28 Jan 2026 09:58:35 +0000
Subject: [PATCH 14/18] Add unit tests for XMLTokener.unescapeEntity() input
 validation

Added comprehensive test coverage for numeric character reference parsing:

Exception cases (should throw JSONException):
- Empty numeric entity: &#;
- Invalid decimal entity: &#txx;
- Empty hex entity: &#x;
- Invalid hex characters: &#xGGG;

Valid cases (should parse correctly):
- Decimal entity: A -> 'A'
- Lowercase hex entity: A -> 'A'
- Uppercase hex entity: A -> 'A'

These tests verify the fixes for issues #1035 and #1036.
---
 src/test/java/org/json/junit/XMLTest.java | 75 +++++++++++++++++++++++
 1 file changed, 75 insertions(+)

diff --git a/src/test/java/org/json/junit/XMLTest.java b/src/test/java/org/json/junit/XMLTest.java
index 2fa5daeea..25b0a0e42 100644
--- a/src/test/java/org/json/junit/XMLTest.java
+++ b/src/test/java/org/json/junit/XMLTest.java
@@ -1426,6 +1426,81 @@ public void clarifyCurrentBehavior() {
         assertEquals(jsonObject3.getJSONObject("color").getString("value"), "008E97");
     }
 
+    /**
+     * Tests that empty numeric character reference &#; throws JSONException.
+     * Previously threw StringIndexOutOfBoundsException.
+     * Related to issue #1035
+     */
+    @Test(expected = JSONException.class)
+    public void testEmptyNumericEntityThrowsJSONException() {
+        String xmlStr = "&#;";
+        XML.toJSONObject(xmlStr);
+    }
+
+    /**
+     * Tests that malformed decimal entity &#txx; throws JSONException.
+     * Previously threw NumberFormatException.
+     * Related to issue #1036
+     */
+    @Test(expected = JSONException.class)
+    public void testInvalidDecimalEntityThrowsJSONException() {
+        String xmlStr = "&#txx;";
+        XML.toJSONObject(xmlStr);
+    }
+
+    /**
+     * Tests that empty hex entity &#x; throws JSONException.
+     * Validates proper input validation for hex entities.
+     */
+    @Test(expected = JSONException.class)
+    public void testEmptyHexEntityThrowsJSONException() {
+        String xmlStr = "&#x;";
+        XML.toJSONObject(xmlStr);
+    }
+
+    /**
+     * Tests that invalid hex entity &#xGGG; throws JSONException.
+     * Validates hex digit validation.
+     */
+    @Test(expected = JSONException.class)
+    public void testInvalidHexEntityThrowsJSONException() {
+        String xmlStr = "&#xGGG;";
+        XML.toJSONObject(xmlStr);
+    }
+
+    /**
+     * Tests that valid decimal numeric entity A works correctly.
+     * Should decode to character 'A'.
+     */
+    @Test
+    public void testValidDecimalEntity() {
+        String xmlStr = "A";
+        JSONObject jsonObject = XML.toJSONObject(xmlStr);
+        assertEquals("A", jsonObject.getString("a"));
+    }
+
+    /**
+     * Tests that valid hex numeric entity A works correctly.
+     * Should decode to character 'A'.
+     */
+    @Test
+    public void testValidHexEntity() {
+        String xmlStr = "A";
+        JSONObject jsonObject = XML.toJSONObject(xmlStr);
+        assertEquals("A", jsonObject.getString("a"));
+    }
+
+    /**
+     * Tests that valid uppercase hex entity A works correctly.
+     * Should decode to character 'A'.
+     */
+    @Test
+    public void testValidUppercaseHexEntity() {
+        String xmlStr = "A";
+        JSONObject jsonObject = XML.toJSONObject(xmlStr);
+        assertEquals("A", jsonObject.getString("a"));
+    }
+
 }
 
 

From 0737e04f8a12fc6a1fd26686c03b60374e4ce936 Mon Sep 17 00:00:00 2001
From: OwenSanzas 
Date: Wed, 28 Jan 2026 10:07:34 +0000
Subject: [PATCH 15/18] Add unit tests for JSONML ClassCastException fix

Added comprehensive test coverage for safe type casting:

Exception cases (should throw JSONException, not ClassCastException):
- Malformed XML causing type mismatch in toJSONArray()
- Type mismatch in toJSONObject()

Valid cases (should continue to work):
- Valid XML to JSONArray conversion
- Valid XML to JSONObject conversion

These tests verify the fix for issue #1034 where ClassCastException
was thrown when parse() returned unexpected types.
---
 src/test/java/org/json/junit/JSONMLTest.java | 66 ++++++++++++++++++++
 1 file changed, 66 insertions(+)

diff --git a/src/test/java/org/json/junit/JSONMLTest.java b/src/test/java/org/json/junit/JSONMLTest.java
index 5a360dd59..93a6821d8 100644
--- a/src/test/java/org/json/junit/JSONMLTest.java
+++ b/src/test/java/org/json/junit/JSONMLTest.java
@@ -986,4 +986,70 @@ public void testToJSONObjectMaxNestingDepthWithValidFittingXML() {
         }
     }
 
+    /**
+     * Tests that malformed XML causing type mismatch throws JSONException.
+     * Previously threw ClassCastException when parse() returned String instead of JSONArray.
+     * Related to issue #1034
+     */
+    @Test(expected = JSONException.class)
+    public void testMalformedXMLThrowsJSONExceptionNotClassCast() {
+        // This malformed XML causes parse() to return wrong type
+        byte[] data = {0x3c, 0x0a, 0x2f, (byte)0xff, (byte)0xff, (byte)0xff,
+                      (byte)0xff, (byte)0xff, (byte)0xff, (byte)0xff,
+                      (byte)0xff, 0x3e, 0x42};
+        String xmlStr = new String(data);
+        JSONML.toJSONArray(xmlStr);
+    }
+
+    /**
+     * Tests that type mismatch in toJSONObject throws JSONException.
+     * Validates safe type casting in toJSONObject methods.
+     */
+    @Test
+    public void testToJSONObjectTypeMismatch() {
+        // Create XML that would cause parse() to return wrong type
+        String xmlStr = "<\n/\u00ff\u00ff\u00ff\u00ff\u00ff\u00ff\u00ff\u00ff>B";
+        try {
+            JSONML.toJSONObject(xmlStr);
+            fail("Expected JSONException for type mismatch");
+        } catch (ClassCastException e) {
+            fail("Should throw JSONException, not ClassCastException");
+        } catch (JSONException e) {
+            // Expected - verify it's about type mismatch
+            assertTrue("Exception message should mention type error",
+                e.getMessage().contains("Expected") || e.getMessage().contains("got"));
+        }
+    }
+
+    /**
+     * Tests that valid XML still works correctly after the fix.
+     * Ensures the type checking doesn't break normal operation.
+     */
+    @Test
+    public void testValidXMLStillWorks() {
+        String xmlStr = "value";
+        try {
+            JSONArray jsonArray = JSONML.toJSONArray(xmlStr);
+            assertNotNull("JSONArray should not be null", jsonArray);
+            assertEquals("root", jsonArray.getString(0));
+        } catch (Exception e) {
+            fail("Valid XML should not throw exception: " + e.getMessage());
+        }
+    }
+
+    /**
+     * Tests that valid XML to JSONObject still works correctly.
+     */
+    @Test
+    public void testValidXMLToJSONObjectStillWorks() {
+        String xmlStr = "content";
+        try {
+            JSONObject jsonObject = JSONML.toJSONObject(xmlStr);
+            assertNotNull("JSONObject should not be null", jsonObject);
+            assertEquals("root", jsonObject.getString("tagName"));
+        } catch (Exception e) {
+            fail("Valid XML should not throw exception: " + e.getMessage());
+        }
+    }
+
 }

From 7a8da886e7cc816ddd50f01ca3ae76673d07a671 Mon Sep 17 00:00:00 2001
From: Pratik Tiwari 
Date: Fri, 30 Jan 2026 19:29:46 +0530
Subject: [PATCH 16/18] Remove unnecessary conditions

---
 src/main/java/org/json/XML.java | 4 ++--
 1 file changed, 2 insertions(+), 2 deletions(-)

diff --git a/src/main/java/org/json/XML.java b/src/main/java/org/json/XML.java
index d0216f5d8..539285d8b 100644
--- a/src/main/java/org/json/XML.java
+++ b/src/main/java/org/json/XML.java
@@ -391,7 +391,7 @@ private static boolean parse(XMLTokener x, JSONObject context, String name, XMLP
                             context.append(tagName, JSONObject.NULL);
                         } else if (jsonObject.length() > 0) {
                             context.append(tagName, jsonObject);
-                        } else if(context.isEmpty() && (context.opt(tagName) == null || !(context.get(tagName) instanceof JSONArray))) { //avoids resetting the array in case of an empty tag in the middle or end
+                        } else if(context.isEmpty()) { //avoids resetting the array in case of an empty tag in the middle or end
                             context.put(tagName, new JSONArray());
                         }
                     } else {
@@ -452,7 +452,7 @@ private static boolean parse(XMLTokener x, JSONObject context, String name, XMLP
                                     // Force the value to be an array
                                     if (jsonObject.length() == 0) {
                                         //avoids resetting the array in case of an empty element in the middle or end
-                                        if(context.length()==0 && context.opt(tagName) == null || !(context.get(tagName) instanceof JSONArray)) {
+                                        if(context.isEmpty()) {
                                             context.put(tagName, new JSONArray());
                                         }
                                     } else if (jsonObject.length() == 1

From 510a03ac3609bd226c094f716fce74a44c64c663 Mon Sep 17 00:00:00 2001
From: Pratik Tiwari 
Date: Sat, 31 Jan 2026 10:32:09 +0530
Subject: [PATCH 17/18] Fixes #1040, Aligns non-forceList behaviour with
 forceList

---
 src/main/java/org/json/XML.java               |  6 ++
 .../org/json/junit/XMLConfigurationTest.java  | 63 ++++++++++++++++---
 2 files changed, 59 insertions(+), 10 deletions(-)

diff --git a/src/main/java/org/json/XML.java b/src/main/java/org/json/XML.java
index 539285d8b..7e4b0bb0c 100644
--- a/src/main/java/org/json/XML.java
+++ b/src/main/java/org/json/XML.java
@@ -393,6 +393,11 @@ private static boolean parse(XMLTokener x, JSONObject context, String name, XMLP
                             context.append(tagName, jsonObject);
                         } else if(context.isEmpty()) { //avoids resetting the array in case of an empty tag in the middle or end
                             context.put(tagName, new JSONArray());
+                            if (jsonObject.isEmpty()){
+                                context.append(tagName, "");
+                            }
+                        } else {
+                            context.append(tagName, "");
                         }
                     } else {
                         if (nilAttributeFound) {
@@ -455,6 +460,7 @@ private static boolean parse(XMLTokener x, JSONObject context, String name, XMLP
                                         if(context.isEmpty()) {
                                             context.put(tagName, new JSONArray());
                                         }
+                                        context.append(tagName, "");
                                     } else if (jsonObject.length() == 1
                                             && jsonObject.opt(config.getcDataTagName()) != null) {
                                         context.append(tagName, jsonObject.opt(config.getcDataTagName()));
diff --git a/src/test/java/org/json/junit/XMLConfigurationTest.java b/src/test/java/org/json/junit/XMLConfigurationTest.java
index 8edbe7984..e8ff3b60c 100755
--- a/src/test/java/org/json/junit/XMLConfigurationTest.java
+++ b/src/test/java/org/json/junit/XMLConfigurationTest.java
@@ -1092,7 +1092,7 @@ public void testEmptyForceList() {
                 "";
 
         String expectedStr = 
-                "{\"addresses\":[]}";
+                "{\"addresses\":[\"\"]}";
         
         Set forceList = new HashSet();
         forceList.add("addresses");
@@ -1130,7 +1130,7 @@ public void testEmptyTagForceList() {
                 "";
 
         String expectedStr = 
-                "{\"addresses\":[]}";
+                "{\"addresses\":[\"\"]}";
         
         Set forceList = new HashSet();
         forceList.add("addresses");
@@ -1147,7 +1147,7 @@ public void testEmptyTagForceList() {
     @Test
     public void testForceListWithLastElementAsEmptyTag(){
         final String originalXml = "1";
-        final String expectedJsonString = "{\"root\":{\"id\":[1]}}";
+        final String expectedJsonString = "{\"root\":{\"id\":[1,\"\"]}}";
 
         HashSet forceListCandidates = new HashSet<>();
         forceListCandidates.add("id");
@@ -1163,7 +1163,7 @@ public void testForceListWithLastElementAsEmptyTag(){
     @Test
     public void testForceListWithFirstElementAsEmptyTag(){
         final String originalXml = "1";
-        final String expectedJsonString = "{\"root\":{\"id\":[1]}}";
+        final String expectedJsonString = "{\"root\":{\"id\":[\"\",1]}}";
 
         HashSet forceListCandidates = new HashSet<>();
         forceListCandidates.add("id");
@@ -1179,7 +1179,7 @@ public void testForceListWithFirstElementAsEmptyTag(){
     @Test
     public void testForceListWithMiddleElementAsEmptyTag(){
         final String originalXml = "12";
-        final String expectedJsonString = "{\"root\":{\"id\":[1,2]}}";
+        final String expectedJsonString = "{\"root\":{\"id\":[1,\"\",2]}}";
 
         HashSet forceListCandidates = new HashSet<>();
         forceListCandidates.add("id");
@@ -1195,8 +1195,7 @@ public void testForceListWithMiddleElementAsEmptyTag(){
     @Test
     public void testForceListWithLastElementAsEmpty(){
         final String originalXml = "1";
-        final String expectedJsonString = "{\"root\":{\"id\":[1]}}";
-
+        final String expectedJsonString = "{\"root\":{\"id\":[1,\"\"]}}";
         HashSet forceListCandidates = new HashSet<>();
         forceListCandidates.add("id");
         final JSONObject json = XML.toJSONObject(originalXml,
@@ -1210,7 +1209,7 @@ public void testForceListWithLastElementAsEmpty(){
     @Test
     public void testForceListWithFirstElementAsEmpty(){
         final String originalXml = "1";
-        final String expectedJsonString = "{\"root\":{\"id\":[1]}}";
+        final String expectedJsonString = "{\"root\":{\"id\":[\"\",1]}}";
 
         HashSet forceListCandidates = new HashSet<>();
         forceListCandidates.add("id");
@@ -1225,7 +1224,7 @@ public void testForceListWithFirstElementAsEmpty(){
     @Test
     public void testForceListWithMiddleElementAsEmpty(){
         final String originalXml = "12";
-        final String expectedJsonString = "{\"root\":{\"id\":[1,2]}}";
+        final String expectedJsonString = "{\"root\":{\"id\":[1,\"\",2]}}";
 
         HashSet forceListCandidates = new HashSet<>();
         forceListCandidates.add("id");
@@ -1240,7 +1239,7 @@ public void testForceListWithMiddleElementAsEmpty(){
     @Test
     public void testForceListEmptyAndEmptyTagsMixed(){
         final String originalXml = "12";
-        final String expectedJsonString = "{\"root\":{\"id\":[1,2]}}";
+        final String expectedJsonString = "{\"root\":{\"id\":[\"\",\"\",1,\"\",\"\",2]}}";
 
         HashSet forceListCandidates = new HashSet<>();
         forceListCandidates.add("id");
@@ -1252,6 +1251,50 @@ public void testForceListEmptyAndEmptyTagsMixed(){
         assertEquals(expectedJsonString, json.toString());
     }
 
+    @Test
+    public void testForceListConsistencyWithDefault() {
+        final String originalXml = "01";
+        final String expectedJsonString = "{\"root\":{\"id\":[0,1,\"\",\"\"]}}";
+
+        // confirm expected result of default array-of-tags processing
+        JSONObject json = XML.toJSONObject(originalXml);
+        assertEquals(expectedJsonString, json.toString());
+
+        // confirm forceList array-of-tags processing is consistent with default processing
+        HashSet forceListCandidates = new HashSet<>();
+        forceListCandidates.add("id");
+        json = XML.toJSONObject(originalXml,
+                new XMLParserConfiguration()
+                        .withForceList(forceListCandidates));
+        assertEquals(expectedJsonString, json.toString());
+    }
+
+    @Test
+    public void testForceListInitializesAnArrayWithAnEmptyElement(){
+        final String originalXml = "";
+        final String expectedJsonString = "{\"root\":{\"id\":[\"\"]}}";
+
+        HashSet forceListCandidates = new HashSet<>();
+        forceListCandidates.add("id");
+        JSONObject json = XML.toJSONObject(originalXml,
+                new XMLParserConfiguration()
+                        .withForceList(forceListCandidates));
+        assertEquals(expectedJsonString, json.toString());
+    }
+
+    @Test
+    public void testForceListInitializesAnArrayWithAnEmptyTag(){
+        final String originalXml = "";
+        final String expectedJsonString = "{\"root\":{\"id\":[\"\"]}}";
+
+        HashSet forceListCandidates = new HashSet<>();
+        forceListCandidates.add("id");
+        JSONObject json = XML.toJSONObject(originalXml,
+                new XMLParserConfiguration()
+                        .withForceList(forceListCandidates));
+        assertEquals(expectedJsonString, json.toString());
+    }
+
     @Test
     public void testMaxNestingDepthIsSet() {
         XMLParserConfiguration xmlParserConfiguration = XMLParserConfiguration.ORIGINAL;

From ff264ef647066c60d176589d787ea5fe5981ed74 Mon Sep 17 00:00:00 2001
From: Sean Leary 
Date: Wed, 18 Feb 2026 14:50:17 -0600
Subject: [PATCH 18/18] Enhance README with license clarification

Added license clarification
---
 README.md | 12 +++++++++++-
 1 file changed, 11 insertions(+), 1 deletion(-)

diff --git a/README.md b/README.md
index 5cc3bd451..47465b134 100644
--- a/README.md
+++ b/README.md
@@ -20,6 +20,8 @@ JSON in Java [package org.json]
 
 The JSON-Java package is a reference implementation that demonstrates how to parse JSON documents into Java objects and how to generate new JSON documents from the Java classes.
 
+The files in this package implement JSON encoders and decoders. The package can also convert between JSON and XML, HTTP headers, Cookies, and CDL.
+
 Project goals include:
 * Reliable and consistent results
 * Adherence to the JSON specification 
@@ -29,8 +31,16 @@ Project goals include:
 * Maintain backward compatibility
 * Designed and tested to use on Java versions 1.6 - 25
 
+# License Clarification
+This project is in the public domain. This means:
+* You can use this code for any purpose, including commercial projects
+* No attribution or credit is required
+* You can modify, distribute, and sublicense freely
+* There are no conditions or restrictions whatsoever
+
+We recognize this can create uncertainty for some corporate legal departments accustomed to standard licenses like MIT or Apache 2.0.
+If your organization requires a named license for compliance purposes, public domain is functionally equivalent to the Unlicense or CC0 1.0, both of which have been reviewed and accepted by organizations including the Open Source Initiative and Creative Commons. You may reference either when explaining this project's terms to your legal team.
 
-The files in this package implement JSON encoders and decoders. The package can also convert between JSON and XML, HTTP headers, Cookies, and CDL.
 
 # If you would like to contribute to this project