mirror of
https://github.com/stleary/JSON-java.git
synced 2026-01-24 00:03:17 -05:00
Merge pull request #1020 from Abhineshhh/fix/support-java-records
Fix: Support Java record accessors in JSONObject
This commit is contained in:
@@ -144,6 +144,18 @@ public class JSONObject {
|
|||||||
*/
|
*/
|
||||||
public static final Object NULL = new Null();
|
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<String> EXCLUDED_RECORD_METHOD_NAMES = Collections.unmodifiableSet(
|
||||||
|
new HashSet<String>(Arrays.asList(
|
||||||
|
"get", "is", "set",
|
||||||
|
"toString", "hashCode", "equals", "clone",
|
||||||
|
"notify", "notifyAll", "wait"
|
||||||
|
))
|
||||||
|
);
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Construct an empty JSONObject.
|
* Construct an empty JSONObject.
|
||||||
*/
|
*/
|
||||||
@@ -1823,11 +1835,14 @@ public class JSONObject {
|
|||||||
Class<?> klass = bean.getClass();
|
Class<?> klass = bean.getClass();
|
||||||
|
|
||||||
// If klass is a System class then set includeSuperClass to false.
|
// 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);
|
Method[] methods = getMethods(klass);
|
||||||
for (final Method method : methods) {
|
for (final Method method : methods) {
|
||||||
if (isValidMethod(method)) {
|
if (isValidMethod(method)) {
|
||||||
final String key = getKeyNameFromMethod(method);
|
final String key = getKeyNameFromMethod(method, isRecord);
|
||||||
if (key != null && !key.isEmpty()) {
|
if (key != null && !key.isEmpty()) {
|
||||||
processMethod(bean, objectsRecord, jsonParserConfiguration, method, key);
|
processMethod(bean, objectsRecord, jsonParserConfiguration, method, key);
|
||||||
}
|
}
|
||||||
@@ -1873,6 +1888,29 @@ public class JSONObject {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 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
|
* This is a convenience method to simplify populate maps
|
||||||
* @param klass the name of the object being checked
|
* @param klass the name of the object being checked
|
||||||
@@ -1885,10 +1923,11 @@ public class JSONObject {
|
|||||||
}
|
}
|
||||||
|
|
||||||
private static boolean isValidMethodName(String name) {
|
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);
|
final int ignoreDepth = getAnnotationDepth(method, JSONPropertyIgnore.class);
|
||||||
if (ignoreDepth > 0) {
|
if (ignoreDepth > 0) {
|
||||||
final int forcedNameDepth = getAnnotationDepth(method, JSONPropertyName.class);
|
final int forcedNameDepth = getAnnotationDepth(method, JSONPropertyName.class);
|
||||||
@@ -1909,6 +1948,11 @@ public class JSONObject {
|
|||||||
} else if (name.startsWith("is") && name.length() > 2) {
|
} else if (name.startsWith("is") && name.length() > 2) {
|
||||||
key = name.substring(2);
|
key = name.substring(2);
|
||||||
} else {
|
} 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;
|
return null;
|
||||||
}
|
}
|
||||||
// if the first letter in the key is not uppercase, then skip.
|
// if the first letter in the key is not uppercase, then skip.
|
||||||
@@ -1925,6 +1969,37 @@ public class JSONObject {
|
|||||||
return key;
|
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.
|
* 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
|
* @param annotation the annotation to check
|
||||||
|
|||||||
179
src/test/java/org/json/junit/JSONObjectRecordTest.java
Normal file
179
src/test/java/org/json/junit/JSONObjectRecordTest.java
Normal file
@@ -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());
|
||||||
|
}
|
||||||
|
}
|
||||||
31
src/test/java/org/json/junit/data/PersonRecord.java
Normal file
31
src/test/java/org/json/junit/data/PersonRecord.java
Normal file
@@ -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;
|
||||||
|
}
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user