diff --git a/.github/workflows/pipeline.yml b/.github/workflows/pipeline.yml index d59702cae..85aea5501 100644 --- a/.github/workflows/pipeline.yml +++ b/.github/workflows/pipeline.yml @@ -15,7 +15,7 @@ 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 with: @@ -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,56 @@ 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 + + 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 + diff --git a/README.md b/README.md index 28f71971e..5cc3bd451 100644 --- a/README.md +++ b/README.md @@ -9,8 +9,9 @@ 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)** +**[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 @@ -26,7 +27,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. 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 diff --git a/src/main/java/org/json/JSONObject.java b/src/main/java/org/json/JSONObject.java index 4e8b42c97..db2c2aac7 100644 --- a/src/main/java/org/json/JSONObject.java +++ b/src/main/java/org/json/JSONObject.java @@ -144,6 +144,18 @@ public Class getMapType() { */ public static final Object NULL = new Null(); + /** + * Set of method names that should be excluded when identifying record-style accessors. + * These are common bean/Object method names that are not property accessors. + */ + private static final Set EXCLUDED_RECORD_METHOD_NAMES = Collections.unmodifiableSet( + new HashSet(Arrays.asList( + "get", "is", "set", + "toString", "hashCode", "equals", "clone", + "notify", "notifyAll", "wait" + )) + ); + /** * Construct an empty JSONObject. */ @@ -1823,11 +1835,14 @@ private void populateMap(Object bean, Set objectsRecord, JSONParserConfi Class klass = bean.getClass(); // If klass is a System class then set includeSuperClass to false. + + // Check if this is a Java record type + boolean isRecord = isRecordType(klass); Method[] methods = getMethods(klass); for (final Method method : methods) { if (isValidMethod(method)) { - final String key = getKeyNameFromMethod(method); + final String key = getKeyNameFromMethod(method, isRecord); if (key != null && !key.isEmpty()) { processMethod(bean, objectsRecord, jsonParserConfiguration, method, key); } @@ -1873,6 +1888,29 @@ private void processMethod(Object bean, Set objectsRecord, JSONParserCon } } + /** + * Checks if a class is a Java record type. + * This uses reflection to check for the isRecord() method which was introduced in Java 16. + * This approach works even when running on Java 6+ JVM. + * + * @param klass the class to check + * @return true if the class is a record type, false otherwise + */ + private static boolean isRecordType(Class klass) { + try { + // Use reflection to check if Class has an isRecord() method (Java 16+) + // This allows the code to compile on Java 6 while still detecting records at runtime + Method isRecordMethod = Class.class.getMethod("isRecord"); + return (Boolean) isRecordMethod.invoke(klass); + } catch (NoSuchMethodException e) { + // isRecord() method doesn't exist - we're on Java < 16 + return false; + } catch (Exception e) { + // Any other reflection error - assume not a record + return false; + } + } + /** * This is a convenience method to simplify populate maps * @param klass the name of the object being checked @@ -1885,10 +1923,11 @@ private static Method[] getMethods(Class klass) { } private static boolean isValidMethodName(String name) { - return !"getClass".equals(name) && !"getDeclaringClass".equals(name); + return !"getClass".equals(name) + && !"getDeclaringClass".equals(name); } - private static String getKeyNameFromMethod(Method method) { + private static String getKeyNameFromMethod(Method method, boolean isRecordType) { final int ignoreDepth = getAnnotationDepth(method, JSONPropertyIgnore.class); if (ignoreDepth > 0) { final int forcedNameDepth = getAnnotationDepth(method, JSONPropertyName.class); @@ -1909,6 +1948,11 @@ private static String getKeyNameFromMethod(Method method) { } else if (name.startsWith("is") && name.length() > 2) { key = name.substring(2); } else { + // Only check for record-style accessors if this is actually a record type + // This maintains backward compatibility - classes with lowercase methods won't be affected + if (isRecordType && isRecordStyleAccessor(name, method)) { + return name; + } return null; } // if the first letter in the key is not uppercase, then skip. @@ -1925,6 +1969,37 @@ private static String getKeyNameFromMethod(Method method) { return key; } + /** + * Checks if a method is a record-style accessor. + * Record accessors have lowercase names without get/is prefixes and are not inherited from standard Java classes. + * + * @param methodName the name of the method + * @param method the method to check + * @return true if this is a record-style accessor, false otherwise + */ + private static boolean isRecordStyleAccessor(String methodName, Method method) { + if (methodName.isEmpty() || !Character.isLowerCase(methodName.charAt(0))) { + return false; + } + + // Exclude common bean/Object method names + if (EXCLUDED_RECORD_METHOD_NAMES.contains(methodName)) { + return false; + } + + Class declaringClass = method.getDeclaringClass(); + if (declaringClass == null || declaringClass == Object.class) { + return false; + } + + if (Enum.class.isAssignableFrom(declaringClass) || Number.class.isAssignableFrom(declaringClass)) { + return false; + } + + String className = declaringClass.getName(); + return !className.startsWith("java.") && !className.startsWith("javax."); + } + /** * checks if the annotation is not null and the {@link JSONPropertyName#value()} is not null and is not empty. * @param annotation the annotation to check 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/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/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; + } +}