Compare commits

...

27 Commits

Author SHA1 Message Date
Sean Leary d749ee16ab Merge pull request #1053 from stleary/update-security-md-with-key
update security.md with key data
2026-05-15 21:35:37 -05:00
Sean Leary 649598338f update-security-md-with-key new security.md file, also fixed 1000 level jsonarray test that fails on my laptop 2026-05-11 11:54:52 -05:00
Sean Leary 896ce0fb74 Merge pull request #1046 from yuki-matsuhashi/master
Validate XML numeric character references before string construction
2026-03-26 11:52:49 -05:00
Yuki Matsuhashi 1877069780 Validate XML numeric character references before string construction 2026-03-24 04:22:50 +09:00
Sean Leary b959027aa2 Merge pull request #1044 from yuki-matsuhashi/1043-ignore-static
Ignore static fields in JSONObject.fromJson()
2026-03-16 09:24:24 -05:00
Yuki Matsuhashi 039f331d7d Add comment for empty test constructor 2026-03-13 01:54:58 +09:00
Yuki Matsuhashi 94e340002b Ignore static fields in JSONObject.fromJson() 2026-03-13 01:23:59 +09:00
Sean Leary 6230128f59 Merge pull request #1041 from stleary/license-clarification
Enhance README with license clarification
2026-02-22 15:14:00 -06:00
Sean Leary ff264ef647 Enhance README with license clarification
Added license clarification
2026-02-18 14:50:17 -06:00
Sean Leary a37aa69480 Merge pull request #1039 from pratiktiwari13/bugfix/empty-force-list
Fixes the issue of losing the array if an empty forceList element or a tag is in the middle or the end
2026-02-03 11:15:58 -06:00
Pratik Tiwari 510a03ac36 Fixes #1040, Aligns non-forceList behaviour with forceList 2026-01-31 10:34:24 +05:30
Sean Leary 538afc3d78 Merge pull request #1038 from OwenSanzas/fix-xmltokener-unescapeentity
Fix input validation in XMLTokener.unescapeEntity()
2026-01-30 08:13:34 -06:00
Sean Leary d092d0903c Merge pull request #1037 from OwenSanzas/fix-jsonml-classcast
Fix ClassCastException in JSONML.toJSONArray and toJSONObject
2026-01-30 08:12:16 -06:00
Pratik Tiwari 7a8da886e7 Remove unnecessary conditions 2026-01-30 19:29:46 +05:30
OwenSanzas 0737e04f8a 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.
2026-01-28 10:07:34 +00:00
OwenSanzas 592e7828d9 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.
2026-01-28 09:58:35 +00:00
OwenSanzas 6c1bfbc7a5 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.
2026-01-28 09:52:25 +00:00
OwenSanzas 534ce3c4d1 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
2026-01-27 11:40:18 +00:00
OwenSanzas 9d14246bee 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
2026-01-27 11:36:46 +00:00
Pratik Tiwari 995fb840f7 Fixes the issue of losing the array if an empty forceList element or a tag is in the middle or the end 2026-01-02 21:20:53 +05:30
Sean Leary e635f40238 Merge pull request #1027 from Simulant87/1023-set-default-locale
Save/restore default locale in test
2025-12-29 19:42:57 -06:00
Sean Leary d5e744ca90 Merge pull request #1028 from Simulant87/fix-sonarqube-reliability-issues
Refactoring: Fix sonarqube reliability issues
2025-12-29 19:42:02 -06:00
Sean Leary e0c4086168 Merge pull request #1029 from Simulant87/external-javadoc-badge
add badge to external hosted javadoc
2025-12-29 19:41:31 -06:00
Sean Leary cf653682be Merge pull request #1030 from stleary/pre-release-20251224
pre-release-20251224 Prep for next release
2025-12-24 09:16:47 -06:00
Simulant 96353de304 add badge to external hosted javadoc 2025-12-21 23:16:01 +01:00
Simulant 8cbb4d5bb3 Fix sonarqube reliability issues 2025-12-20 22:57:24 +01:00
Simulant 421abfdc1f save and restore the current default locale, to avoid any side effects on other executions in the same JVM 2025-12-20 22:27:45 +01:00
14 changed files with 623 additions and 65 deletions
+3
View File
@@ -16,3 +16,6 @@ build
/gradlew
/gradlew.bat
.gitmodules
# ignore compiled class files
*.class
+12 -1
View File
@@ -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/20251224/json-20251224.jar)**
@@ -19,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
@@ -28,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
+55
View File
@@ -3,3 +3,58 @@
## Reporting a Vulnerability
Please follow the instructions in the ["How are vulnerabilities and exploits handled?"](https://github.com/stleary/JSON-java/wiki/FAQ#how-are-vulnerabilities-and-exploits-handled) section in the FAQ.
## Verifying Release Signatures
All releases of `org.json:json` published to Maven Central are signed with PGP. The fingerprint, keyserver location, and verification procedure below let you confirm that the artifacts you've downloaded were produced by this project and have not been modified in transit.
### Signing Key
| | |
| --- | --- |
| **Fingerprint** | `FB35 C8D0 2B47 24DA DA23 DE0A FD11 6C19 69FC CFF3` |
| **Long key ID** | `FD116C1969FCCFF3` |
| **Keyserver** | `hkps://keyserver.ubuntu.com` |
The full 40-character fingerprint above is the canonical identifier for the key. Always pin or compare against the full fingerprint rather than the long or short key ID.
### Importing the Key
```bash
gpg --keyserver hkps://keyserver.ubuntu.com \
--recv-keys FB35C8D02B4724DADA23DE0AFD116C1969FCCFF3
```
After importing, confirm the fingerprint matches what's published here:
```bash
gpg --fingerprint FB35C8D02B4724DADA23DE0AFD116C1969FCCFF3
```
### Verifying an Artifact
Download both the artifact and its detached signature from Maven Central. For example, for version `20251224`:
```bash
curl -O https://repo1.maven.org/maven2/org/json/json/20251224/json-20251224.jar
curl -O https://repo1.maven.org/maven2/org/json/json/20251224/json-20251224.jar.asc
gpg --verify json-20251224.jar.asc json-20251224.jar
```
A successful verification will report `Good signature from ...` and display the same fingerprint shown above. If GPG reports `BAD signature`, a mismatched fingerprint, or `No public key`, do not use the artifact and please open an issue.
The same procedure applies to the `.pom` and any other signed sidecars in the release directory; substitute the filename you want to verify.
### Gradle Dependency Verification
If you are using Gradle's [dependency verification](https://docs.gradle.org/current/userguide/dependency_verification.html) feature, add an entry like the following to `gradle/verification-metadata.xml`:
```xml
<trusted-key id="FB35C8D02B4724DADA23DE0AFD116C1969FCCFF3" group="org.json" name="json"/>
```
Gradle also accepts the long key ID (`FD116C1969FCCFF3`), but pinning the full fingerprint is recommended.
### Key Rotation
If the signing key is ever rotated or revoked, this document will be updated in the `master` branch with the new fingerprint, and the change will be visible in the file's commit history. Always check this file directly in the repository for the current authoritative value before trusting any third-party copy of the fingerprint.
+39 -12
View File
@@ -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 @@ public class JSONML {
* @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 class JSONML {
* @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 class JSONML {
* @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 class JSONML {
* @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 class JSONML {
* @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 class JSONML {
* @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 class JSONML {
* @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 class JSONML {
* @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 class JSONML {
* @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 class JSONML {
* @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 class JSONML {
* @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 class JSONML {
* @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));
}
+4 -1
View File
@@ -3349,7 +3349,7 @@ public class JSONObject {
* of the given class. It supports basic data types including {@code int}, {@code double},
* {@code float}, {@code long}, and {@code boolean}, as well as their boxed counterparts.
* The target class must have a no-argument constructor, and its field names must match
* the keys in the JSON string.
* the keys in the JSON string. Static fields are ignored.
*
* <p><strong>Note:</strong> Only classes that are explicitly supported and registered within
* the {@code JSONObject} context can be deserialized. If the provided class is not among those,
@@ -3366,6 +3366,9 @@ public class JSONObject {
try {
T obj = clazz.getDeclaredConstructor().newInstance();
for (Field field : clazz.getDeclaredFields()) {
if (Modifier.isStatic(field.getModifiers())) {
continue;
}
field.setAccessible(true);
String fieldName = field.getName();
if (has(fieldName)) {
+16 -3
View File
@@ -9,6 +9,7 @@ import java.io.StringReader;
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 @@ public class XML {
public Iterator<Integer> iterator() {
return new Iterator<Integer>() {
private int nextIndex = 0;
private int length = string.length();
private final int length = string.length();
@Override
public boolean hasNext() {
@@ -89,6 +90,9 @@ public class XML {
@Override
public Integer next() {
if (!hasNext()) {
throw new NoSuchElementException();
}
int result = string.codePointAt(this.nextIndex);
this.nextIndex += Character.charCount(result);
return result;
@@ -154,7 +158,7 @@ public class XML {
* @param cp code point to test
* @return true if the code point is not valid for an XML
*/
private static boolean mustEscape(int cp) {
static boolean mustEscape(int cp) {
/* Valid range from https://www.w3.org/TR/REC-xml/#charsets
*
* #x9 | #xA | #xD | [#x20-#xD7FF] | [#xE000-#xFFFD] | [#x10000-#x10FFFF]
@@ -387,8 +391,13 @@ public class XML {
context.append(tagName, JSONObject.NULL);
} else if (jsonObject.length() > 0) {
context.append(tagName, jsonObject);
} else {
} 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) {
@@ -447,7 +456,11 @@ public class XML {
if (config.getForceList().contains(tagName)) {
// 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.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()));
+87 -9
View File
@@ -151,22 +151,24 @@ public class XMLTokener extends JSONTokener {
/**
* 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 "";
}
// if our entity is an encoded unicode point, parse it.
if (e.charAt(0) == '#') {
int cp;
if (e.charAt(1) == 'x' || e.charAt(1) == 'X') {
// hex encoded unicode
cp = Integer.parseInt(e.substring(2), 16);
} else {
// decimal encoded unicode
cp = Integer.parseInt(e.substring(1));
if (e.length() < 2) {
throw new JSONException("Invalid numeric character reference: &#;");
}
int cp = (e.charAt(1) == 'x' || e.charAt(1) == 'X')
? parseHexEntity(e)
: parseDecimalEntity(e);
if (XML.mustEscape(cp)) {
throw new JSONException("Invalid numeric character reference: &#" + e.substring(1) + ";");
}
return new String(new int[] {cp}, 0, 1);
}
@@ -178,6 +180,82 @@ public class XMLTokener extends JSONTokener {
return knownEntity.toString();
}
/**
* Parse a hexadecimal numeric character reference (e.g., "&#xABC;").
* @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., "&#123;").
* @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
* @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;
}
/**
* <pre>{@code
@@ -1502,19 +1502,23 @@ public class JSONArrayTest {
}
@Test
public void testRecursiveDepthArrayFor1000Levels() {
/**
* This test was originally for 1000 levels, which passes in test builds, but fails on my laptop.
* The current value of 900 seems to work.
*/
public void testRecursiveDepthArrayFor900Levels() {
try {
ArrayList<Object> array = buildNestedArray(1000);
JSONParserConfiguration parserConfiguration = new JSONParserConfiguration().withMaxNestingDepth(1000);
ArrayList<Object> array = buildNestedArray(900);
JSONParserConfiguration parserConfiguration = new JSONParserConfiguration().withMaxNestingDepth(900);
new JSONArray(array, parserConfiguration);
} catch (StackOverflowError e) {
String javaVersion = System.getProperty("java.version");
if (javaVersion.startsWith("11.")) {
System.out.println(
"testRecursiveDepthArrayFor1000Levels() allowing intermittent stackoverflow, Java Version: "
"testRecursiveDepthArrayFor900Levels() allowing intermittent stackoverflow, Java Version: "
+ javaVersion);
} else {
String errorStr = "testRecursiveDepthArrayFor1000Levels() unexpected stackoverflow, Java Version: "
String errorStr = "testRecursiveDepthArrayFor900Levels() unexpected stackoverflow, Java Version: "
+ javaVersion;
System.out.println(errorStr);
throw new RuntimeException(errorStr);
@@ -986,4 +986,70 @@ public class JSONMLTest {
}
}
/**
* 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 = "<root><item>value</item></root>";
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 = "<root attr=\"value\"><item>content</item></root>";
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());
}
}
}
@@ -36,6 +36,9 @@ public class JSONObjectLocaleTest {
MyLocaleBean myLocaleBean = new MyLocaleBean();
// 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.
@@ -56,5 +59,8 @@ public class JSONObjectLocaleTest {
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);
}
}
}
@@ -66,6 +66,7 @@ import org.json.junit.data.CustomClassF;
import org.json.junit.data.CustomClassG;
import org.json.junit.data.CustomClassH;
import org.json.junit.data.CustomClassI;
import org.json.junit.data.CustomClassJ;
import org.json.JSONObject;
import org.junit.After;
import org.junit.Ignore;
@@ -3117,12 +3118,13 @@ public class JSONObjectTest {
// 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());
@@ -3136,14 +3138,15 @@ public class JSONObjectTest {
// 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());
@@ -4230,4 +4233,21 @@ public class JSONObjectTest {
CustomClassI compareClassI = new CustomClassI(dataList);
assertEquals(customClassI.integerMap.toString(), compareClassI.integerMap.toString());
}
@Test
public void jsonObjectParseFromJson_9() {
JSONObject object = new JSONObject();
object.put("number", 12);
object.put("classState", "mutated");
String initialClassState = CustomClassJ.classState;
CustomClassJ.classState = "original";
try {
CustomClassJ customClassJ = object.fromJson(CustomClassJ.class);
assertEquals(12, customClassJ.number);
assertEquals("original", CustomClassJ.classState);
} finally {
CustomClassJ.classState = initialClassState;
}
}
}
@@ -1092,7 +1092,7 @@ public class XMLConfigurationTest {
"<addresses></addresses>";
String expectedStr =
"{\"addresses\":[]}";
"{\"addresses\":[\"\"]}";
Set<String> forceList = new HashSet<String>();
forceList.add("addresses");
@@ -1130,7 +1130,7 @@ public class XMLConfigurationTest {
"<addresses />";
String expectedStr =
"{\"addresses\":[]}";
"{\"addresses\":[\"\"]}";
Set<String> forceList = new HashSet<String>();
forceList.add("addresses");
@@ -1144,6 +1144,157 @@ public class XMLConfigurationTest {
Util.compareActualVsExpectedJsonObjects(jsonObject, expetedJsonObject);
}
@Test
public void testForceListWithLastElementAsEmptyTag(){
final String originalXml = "<root><id>1</id><id/></root>";
final String expectedJsonString = "{\"root\":{\"id\":[1,\"\"]}}";
HashSet<String> 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 = "<root><id/><id>1</id></root>";
final String expectedJsonString = "{\"root\":{\"id\":[\"\",1]}}";
HashSet<String> 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 = "<root><id>1</id><id/><id>2</id></root>";
final String expectedJsonString = "{\"root\":{\"id\":[1,\"\",2]}}";
HashSet<String> 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 = "<root><id>1</id><id></id></root>";
final String expectedJsonString = "{\"root\":{\"id\":[1,\"\"]}}";
HashSet<String> 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 = "<root><id></id><id>1</id></root>";
final String expectedJsonString = "{\"root\":{\"id\":[\"\",1]}}";
HashSet<String> 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 = "<root><id>1</id><id></id><id>2</id></root>";
final String expectedJsonString = "{\"root\":{\"id\":[1,\"\",2]}}";
HashSet<String> 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 = "<root><id></id><id/><id>1</id><id/><id></id><id>2</id></root>";
final String expectedJsonString = "{\"root\":{\"id\":[\"\",\"\",1,\"\",\"\",2]}}";
HashSet<String> 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 testForceListConsistencyWithDefault() {
final String originalXml = "<root><id>0</id><id>1</id><id/><id></id></root>";
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<String> 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 = "<root><id></id></root>";
final String expectedJsonString = "{\"root\":{\"id\":[\"\"]}}";
HashSet<String> 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 = "<root><id/></root>";
final String expectedJsonString = "{\"root\":{\"id\":[\"\"]}}";
HashSet<String> 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;
+111
View File
@@ -1426,6 +1426,117 @@ public class XMLTest {
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 = "<a>&#;</a>";
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 = "<a>&#txx;</a>";
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 = "<a>&#x;</a>";
XML.toJSONObject(xmlStr);
}
/**
* Tests that invalid hex entity &#xGGG; throws JSONException.
* Validates hex digit validation.
*/
@Test(expected = JSONException.class)
public void testInvalidHexEntityThrowsJSONException() {
String xmlStr = "<a>&#xGGG;</a>";
XML.toJSONObject(xmlStr);
}
/**
* Tests that out-of-range hex entities throw JSONException rather than an uncaught runtime exception.
*/
@Test(expected = JSONException.class)
public void testOutOfRangeHexEntityThrowsJSONException() {
String xmlStr = "<a>&#x110000;</a>";
XML.toJSONObject(xmlStr);
}
/**
* Tests that out-of-range decimal entities throw JSONException rather than an uncaught runtime exception.
*/
@Test(expected = JSONException.class)
public void testOutOfRangeDecimalEntityThrowsJSONException() {
String xmlStr = "<a>&#1114112;</a>";
XML.toJSONObject(xmlStr);
}
/**
* Tests that surrogate code point entities throw JSONException.
*/
@Test(expected = JSONException.class)
public void testSurrogateHexEntityThrowsJSONException() {
String xmlStr = "<a>&#xD800;</a>";
XML.toJSONObject(xmlStr);
}
/**
* Tests that out-of-range numeric entities in attribute values throw JSONException.
*/
@Test(expected = JSONException.class)
public void testOutOfRangeHexEntityInAttributeThrowsJSONException() {
String xmlStr = "<a b=\"&#x110000;\"/>";
XML.toJSONObject(xmlStr);
}
/**
* Tests that valid decimal numeric entity &#65; works correctly.
* Should decode to character 'A'.
*/
@Test
public void testValidDecimalEntity() {
String xmlStr = "<a>&#65;</a>";
JSONObject jsonObject = XML.toJSONObject(xmlStr);
assertEquals("A", jsonObject.getString("a"));
}
/**
* Tests that valid hex numeric entity &#x41; works correctly.
* Should decode to character 'A'.
*/
@Test
public void testValidHexEntity() {
String xmlStr = "<a>&#x41;</a>";
JSONObject jsonObject = XML.toJSONObject(xmlStr);
assertEquals("A", jsonObject.getString("a"));
}
/**
* Tests that valid uppercase hex entity &#X41; works correctly.
* Should decode to character 'A'.
*/
@Test
public void testValidUppercaseHexEntity() {
String xmlStr = "<a>&#X41;</a>";
JSONObject jsonObject = XML.toJSONObject(xmlStr);
assertEquals("A", jsonObject.getString("a"));
}
}
@@ -0,0 +1,10 @@
package org.json.junit.data;
public class CustomClassJ {
public static String classState = "original";
public int number;
public CustomClassJ() {
// Required for JSONObject#fromJson(Class<T>) tests.
}
}