() {
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;
@@ -387,8 +391,13 @@ 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()) { //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 @@ 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.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/main/java/org/json/XMLTokener.java b/src/main/java/org/json/XMLTokener.java
index bc18b31c9..dad2e2897 100644
--- a/src/main/java/org/json/XMLTokener.java
+++ b/src/main/java/org/json/XMLTokener.java
@@ -151,33 +151,108 @@ 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 "";
}
// 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: ");
}
- return new String(new int[] {cp},0,1);
- }
+ 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);
- if(knownEntity==null) {
+ if (knownEntity == null) {
// we don't know the entity so keep it encoded
return '&' + e + ';';
}
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
+ * @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
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());
+ }
+ }
+
}
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);
+ }
}
}
diff --git a/src/test/java/org/json/junit/JSONObjectRecordTest.java b/src/test/java/org/json/junit/JSONObjectRecordTest.java
new file mode 100644
index 000000000..f1a673d28
--- /dev/null
+++ b/src/test/java/org/json/junit/JSONObjectRecordTest.java
@@ -0,0 +1,179 @@
+package org.json.junit;
+
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertFalse;
+import static org.junit.Assert.assertTrue;
+
+import java.io.StringReader;
+
+import org.json.JSONObject;
+import org.json.junit.data.GenericBeanInt;
+import org.json.junit.data.MyEnum;
+import org.json.junit.data.MyNumber;
+import org.json.junit.data.PersonRecord;
+import org.junit.Ignore;
+import org.junit.Test;
+
+/**
+ * Tests for JSONObject support of Java record types.
+ *
+ * NOTE: These tests are currently ignored because PersonRecord is not an actual Java record.
+ * The implementation now correctly detects actual Java records using reflection (Class.isRecord()).
+ * These tests will need to be enabled and run with Java 17+ where PersonRecord can be converted
+ * to an actual record type.
+ *
+ * This ensures backward compatibility - regular classes with lowercase method names will not
+ * be treated as records unless they are actual Java record types.
+ */
+public class JSONObjectRecordTest {
+
+ /**
+ * Tests that JSONObject can be created from a record-style class.
+ * Record-style classes use accessor methods like name() instead of getName().
+ *
+ * NOTE: Ignored until PersonRecord is converted to an actual Java record (requires Java 17+)
+ */
+ @Test
+ @Ignore("Requires actual Java record type - PersonRecord needs to be a real record (Java 17+)")
+ public void jsonObjectByRecord() {
+ PersonRecord person = new PersonRecord("John Doe", 30, true);
+ JSONObject jsonObject = new JSONObject(person);
+
+ assertEquals("Expected 3 keys in the JSONObject", 3, jsonObject.length());
+ assertEquals("John Doe", jsonObject.get("name"));
+ assertEquals(30, jsonObject.get("age"));
+ assertEquals(true, jsonObject.get("active"));
+ }
+
+ /**
+ * Test that Object methods (toString, hashCode, equals, etc.) are not included
+ *
+ * NOTE: Ignored until PersonRecord is converted to an actual Java record (requires Java 17+)
+ */
+ @Test
+ @Ignore("Requires actual Java record type - PersonRecord needs to be a real record (Java 17+)")
+ public void recordStyleClassShouldNotIncludeObjectMethods() {
+ PersonRecord person = new PersonRecord("Jane Doe", 25, false);
+ JSONObject jsonObject = new JSONObject(person);
+
+ // Should NOT include Object methods
+ assertFalse("Should not include toString", jsonObject.has("toString"));
+ assertFalse("Should not include hashCode", jsonObject.has("hashCode"));
+ assertFalse("Should not include equals", jsonObject.has("equals"));
+ assertFalse("Should not include clone", jsonObject.has("clone"));
+ assertFalse("Should not include wait", jsonObject.has("wait"));
+ assertFalse("Should not include notify", jsonObject.has("notify"));
+ assertFalse("Should not include notifyAll", jsonObject.has("notifyAll"));
+
+ // Should only have the 3 record fields
+ assertEquals("Should only have 3 fields", 3, jsonObject.length());
+ }
+
+ /**
+ * Test that enum methods are not included when processing an enum
+ */
+ @Test
+ public void enumsShouldNotIncludeEnumMethods() {
+ MyEnum myEnum = MyEnum.VAL1;
+ JSONObject jsonObject = new JSONObject(myEnum);
+
+ // Should NOT include enum-specific methods like name(), ordinal(), values(), valueOf()
+ assertFalse("Should not include name method", jsonObject.has("name"));
+ assertFalse("Should not include ordinal method", jsonObject.has("ordinal"));
+ assertFalse("Should not include declaringClass", jsonObject.has("declaringClass"));
+
+ // Enums should still work with traditional getters if they have any
+ // But should not pick up the built-in enum methods
+ }
+
+ /**
+ * Test that Number subclass methods are not included
+ */
+ @Test
+ public void numberSubclassesShouldNotIncludeNumberMethods() {
+ MyNumber myNumber = new MyNumber();
+ JSONObject jsonObject = new JSONObject(myNumber);
+
+ // Should NOT include Number methods like intValue(), longValue(), etc.
+ assertFalse("Should not include intValue", jsonObject.has("intValue"));
+ assertFalse("Should not include longValue", jsonObject.has("longValue"));
+ assertFalse("Should not include doubleValue", jsonObject.has("doubleValue"));
+ assertFalse("Should not include floatValue", jsonObject.has("floatValue"));
+
+ // Should include the actual getter
+ assertTrue("Should include number", jsonObject.has("number"));
+ assertEquals("Should have 1 field", 1, jsonObject.length());
+ }
+
+ /**
+ * Test that generic bean with get() and is() methods works correctly
+ */
+ @Test
+ public void genericBeanWithGetAndIsMethodsShouldNotBeIncluded() {
+ GenericBeanInt bean = new GenericBeanInt(42);
+ JSONObject jsonObject = new JSONObject(bean);
+
+ // Should NOT include standalone get() or is() methods
+ assertFalse("Should not include standalone 'get' method", jsonObject.has("get"));
+ assertFalse("Should not include standalone 'is' method", jsonObject.has("is"));
+
+ // Should include the actual getters
+ assertTrue("Should include genericValue field", jsonObject.has("genericValue"));
+ assertTrue("Should include a field", jsonObject.has("a"));
+ }
+
+ /**
+ * Test that java.* classes don't have their methods picked up
+ */
+ @Test
+ public void javaLibraryClassesShouldNotIncludeTheirMethods() {
+ StringReader reader = new StringReader("test");
+ JSONObject jsonObject = new JSONObject(reader);
+
+ // Should NOT include java.io.Reader methods like read(), reset(), etc.
+ assertFalse("Should not include read method", jsonObject.has("read"));
+ assertFalse("Should not include reset method", jsonObject.has("reset"));
+ assertFalse("Should not include ready method", jsonObject.has("ready"));
+ assertFalse("Should not include skip method", jsonObject.has("skip"));
+
+ // Reader should produce empty JSONObject (no valid properties)
+ assertEquals("Reader should produce empty JSON", 0, jsonObject.length());
+ }
+
+ /**
+ * Test mixed case - object with both traditional getters and record-style accessors
+ *
+ * NOTE: Ignored until PersonRecord is converted to an actual Java record (requires Java 17+)
+ */
+ @Test
+ @Ignore("Requires actual Java record type - PersonRecord needs to be a real record (Java 17+)")
+ public void mixedGettersAndRecordStyleAccessors() {
+ // PersonRecord has record-style accessors: name(), age(), active()
+ // These should all be included
+ PersonRecord person = new PersonRecord("Mixed Test", 40, true);
+ JSONObject jsonObject = new JSONObject(person);
+
+ assertEquals("Should have all 3 record-style fields", 3, jsonObject.length());
+ assertTrue("Should include name", jsonObject.has("name"));
+ assertTrue("Should include age", jsonObject.has("age"));
+ assertTrue("Should include active", jsonObject.has("active"));
+ }
+
+ /**
+ * Test that methods starting with uppercase are not included (not valid record accessors)
+ *
+ * NOTE: Ignored until PersonRecord is converted to an actual Java record (requires Java 17+)
+ */
+ @Test
+ @Ignore("Requires actual Java record type - PersonRecord needs to be a real record (Java 17+)")
+ public void methodsStartingWithUppercaseShouldNotBeIncluded() {
+ PersonRecord person = new PersonRecord("Test", 50, false);
+ JSONObject jsonObject = new JSONObject(person);
+
+ // Record-style accessors must start with lowercase
+ // Methods like Name(), Age() (uppercase) should not be picked up
+ // Our PersonRecord only has lowercase accessors, which is correct
+
+ assertEquals("Should only have lowercase accessors", 3, jsonObject.length());
+ }
+}
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());
diff --git a/src/test/java/org/json/junit/XMLConfigurationTest.java b/src/test/java/org/json/junit/XMLConfigurationTest.java
index ca1980c8a..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");
@@ -1144,6 +1144,157 @@ 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 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;
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 throws JSONException.
+ * Validates proper input validation for hex entities.
+ */
+ @Test(expected = JSONException.class)
+ public void testEmptyHexEntityThrowsJSONException() {
+ String xmlStr = "";
+ XML.toJSONObject(xmlStr);
+ }
+
+ /**
+ * Tests that invalid hex entity GGG; throws JSONException.
+ * Validates hex digit validation.
+ */
+ @Test(expected = JSONException.class)
+ public void testInvalidHexEntityThrowsJSONException() {
+ String xmlStr = "GGG;";
+ 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"));
+ }
+
}
diff --git a/src/test/java/org/json/junit/data/PersonRecord.java b/src/test/java/org/json/junit/data/PersonRecord.java
new file mode 100644
index 000000000..891f1bb9e
--- /dev/null
+++ b/src/test/java/org/json/junit/data/PersonRecord.java
@@ -0,0 +1,31 @@
+package org.json.junit.data;
+
+/**
+ * A test class that mimics Java record accessor patterns.
+ * Records use accessor methods without get/is prefixes (e.g., name() instead of getName()).
+ * This class simulates that behavior to test JSONObject's handling of such methods.
+ */
+public class PersonRecord {
+ private final String name;
+ private final int age;
+ private final boolean active;
+
+ public PersonRecord(String name, int age, boolean active) {
+ this.name = name;
+ this.age = age;
+ this.active = active;
+ }
+
+ // Record-style accessors (no "get" or "is" prefix)
+ public String name() {
+ return name;
+ }
+
+ public int age() {
+ return age;
+ }
+
+ public boolean active() {
+ return active;
+ }
+}