Skip to content

Commit 20f5200

Browse files
committed
Fix: Support Java record accessors in JSONObject
1 parent 25f355a commit 20f5200

File tree

3 files changed

+79
-1
lines changed

3 files changed

+79
-1
lines changed

src/main/java/org/json/JSONObject.java

Lines changed: 28 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1885,7 +1885,8 @@ private static Method[] getMethods(Class<?> klass) {
18851885
}
18861886

18871887
private static boolean isValidMethodName(String name) {
1888-
return !"getClass".equals(name) && !"getDeclaringClass".equals(name);
1888+
return !"getClass".equals(name)
1889+
&& !"getDeclaringClass".equals(name);
18891890
}
18901891

18911892
private static String getKeyNameFromMethod(Method method) {
@@ -1909,6 +1910,32 @@ private static String getKeyNameFromMethod(Method method) {
19091910
} else if (name.startsWith("is") && name.length() > 2) {
19101911
key = name.substring(2);
19111912
} else {
1913+
// Check if this is a record-style accessor (no prefix)
1914+
// Record accessors are simple method names that match field names
1915+
// They must start with a lowercase letter and should be declared in the class itself
1916+
// (not inherited from Object, Enum, Number, or any java.* class)
1917+
// Also exclude common Object/bean method names
1918+
Class<?> declaringClass = method.getDeclaringClass();
1919+
if (name.length() > 0 && Character.isLowerCase(name.charAt(0))
1920+
&& !"get".equals(name)
1921+
&& !"is".equals(name)
1922+
&& !"set".equals(name)
1923+
&& !"toString".equals(name)
1924+
&& !"hashCode".equals(name)
1925+
&& !"equals".equals(name)
1926+
&& !"clone".equals(name)
1927+
&& !"notify".equals(name)
1928+
&& !"notifyAll".equals(name)
1929+
&& !"wait".equals(name)
1930+
&& declaringClass != null
1931+
&& declaringClass != Object.class
1932+
&& !Enum.class.isAssignableFrom(declaringClass)
1933+
&& !Number.class.isAssignableFrom(declaringClass)
1934+
&& !declaringClass.getName().startsWith("java.")
1935+
&& !declaringClass.getName().startsWith("javax.")) {
1936+
// This is a record-style accessor - return the method name as-is
1937+
return name;
1938+
}
19121939
return null;
19131940
}
19141941
// if the first letter in the key is not uppercase, then skip.

src/test/java/org/json/junit/JSONObjectTest.java

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -51,6 +51,7 @@
5151
import org.json.junit.data.MyNumber;
5252
import org.json.junit.data.MyNumberContainer;
5353
import org.json.junit.data.MyPublicClass;
54+
import org.json.junit.data.PersonRecord;
5455
import org.json.junit.data.RecursiveBean;
5556
import org.json.junit.data.RecursiveBeanEquals;
5657
import org.json.junit.data.Singleton;
@@ -796,6 +797,25 @@ public void jsonObjectByBean3() {
796797
Util.checkJSONObjectMaps(jsonObject);
797798
}
798799

800+
/**
801+
* JSONObject built from a Java record.
802+
* Records use accessor methods without get/is prefixes (e.g., name() instead of getName()).
803+
* This test verifies that JSONObject correctly handles record types.
804+
*/
805+
@Test
806+
public void jsonObjectByRecord() {
807+
PersonRecord person = new PersonRecord("John Doe", 30, true);
808+
JSONObject jsonObject = new JSONObject(person);
809+
810+
// validate JSON
811+
Object doc = Configuration.defaultConfiguration().jsonProvider().parse(jsonObject.toString());
812+
assertTrue("expected 3 top level items", ((Map<?,?>)(JsonPath.read(doc, "$"))).size() == 3);
813+
assertTrue("expected name field", "John Doe".equals(jsonObject.query("/name")));
814+
assertTrue("expected age field", Integer.valueOf(30).equals(jsonObject.query("/age")));
815+
assertTrue("expected active field", Boolean.TRUE.equals(jsonObject.query("/active")));
816+
Util.checkJSONObjectMaps(jsonObject);
817+
}
818+
799819
/**
800820
* A bean is also an object. But in order to test the JSONObject
801821
* ctor that takes an object and a list of names,
Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,31 @@
1+
package org.json.junit.data;
2+
3+
/**
4+
* A test class that mimics Java record accessor patterns.
5+
* Records use accessor methods without get/is prefixes (e.g., name() instead of getName()).
6+
* This class simulates that behavior to test JSONObject's handling of such methods.
7+
*/
8+
public class PersonRecord {
9+
private final String name;
10+
private final int age;
11+
private final boolean active;
12+
13+
public PersonRecord(String name, int age, boolean active) {
14+
this.name = name;
15+
this.age = age;
16+
this.active = active;
17+
}
18+
19+
// Record-style accessors (no "get" or "is" prefix)
20+
public String name() {
21+
return name;
22+
}
23+
24+
public int age() {
25+
return age;
26+
}
27+
28+
public boolean active() {
29+
return active;
30+
}
31+
}

0 commit comments

Comments
 (0)