From c5d0c5afdf21036b9eb6b1d0e54a00d949c56306 Mon Sep 17 00:00:00 2001 From: Lyin-OTTF <3037869661@qq.com> Date: Tue, 20 Oct 2026 23:37:31 +0800 Subject: [PATCH] Clarify fromJson(Class) Javadoc: type registry does not exist The fromJson(Class) Javadoc claimed that only classes "explicitly supported and registered within the JSONObject context" can be deserialized, and declared an IllegalArgumentException for classes outside that set. No such registry exists: fromJson instantiates any class with a no-arg constructor and writes every non-static declared field whose name matches a JSON key, widening access with setAccessible(true). Private and transient fields are populated, and JSONPropertyIgnore does not apply because it only suppresses the bean to JSONObject direction. The binding rules are already released, so changing them here would break existing users. Instead document what actually happens, point out that binding is driven by whatever keys the JSON contains, and replace the non-existent IllegalArgumentException with the JSONException the method really throws. Adds JSONObjectTest#jsonObjectParseFromJson_10 to pin the current binding behavior so the documentation cannot drift from the implementation again. Fixes #1076 --- src/main/java/org/json/JSONObject.java | 20 +++++++--- .../java/org/json/junit/JSONObjectTest.java | 25 ++++++++++++ .../org/json/junit/data/CustomClassK.java | 39 +++++++++++++++++++ 3 files changed, 78 insertions(+), 6 deletions(-) create mode 100644 src/test/java/org/json/junit/data/CustomClassK.java diff --git a/src/main/java/org/json/JSONObject.java b/src/main/java/org/json/JSONObject.java index 3ded23f61..1c329e174 100644 --- a/src/main/java/org/json/JSONObject.java +++ b/src/main/java/org/json/JSONObject.java @@ -3472,12 +3472,15 @@ private Type[] getMapTypes(Type type) { * of the given class. It supports basic data types including int, double, float, * long, and boolean (as well as their boxed counterparts). The class must have a * no-argument constructor, and the field names in the class must match the keys - * in the JSON string. + * in the JSON string. Static fields are ignored; see {@link #fromJson(Class)} for + * how the remaining fields are bound. * * @param the type of the object to return * @param jsonString json in string format * @param clazz the class of the object to be returned * @return an instance of Object T with fields populated from the JSON string + * @throws JSONException if jsonString is not valid JSON, or if it cannot be bound to clazz + * as described in {@link #fromJson(Class)} */ public static T fromJson(String jsonString, Class clazz) { JSONObject jsonObject = new JSONObject(jsonString); @@ -3493,15 +3496,20 @@ public static T fromJson(String jsonString, Class clazz) { * The target class must have a no-argument constructor, and its field names must match * the keys in the JSON string. Static fields are ignored. * - *

Note: Only classes that are explicitly supported and registered within - * the {@code JSONObject} context can be deserialized. If the provided class is not among those, - * this method will not be able to deserialize it. This ensures that only a limited and - * controlled set of types can be instantiated from JSON for safety and predictability. + *

Note: binding is driven by the keys present in the JSON, not by + * how the fields are declared. Every non-static field declared by the class is written + * when its name matches a key, including {@code private} and {@code transient} fields, + * and field access is widened with + * {@link java.lang.reflect.Field#setAccessible(boolean)}. {@link JSONPropertyIgnore} + * only suppresses the bean to JSONObject direction, so it does not keep a field from + * being populated here. JSON coming from an untrusted source should therefore be + * validated, or reduced to the expected keys, before it is handed to this method. * * @param clazz the class of the object to be returned * @param the type of the object * @return an instance of type {@code T} with fields populated from the JSON string - * @throws IllegalArgumentException if the class is not supported for deserialization + * @throws JSONException if the class has no accessible no-argument constructor, or if a + * field cannot be instantiated or set */ @SuppressWarnings("unchecked") public T fromJson(Class clazz) { diff --git a/src/test/java/org/json/junit/JSONObjectTest.java b/src/test/java/org/json/junit/JSONObjectTest.java index a0f0638b0..cc6c7c782 100644 --- a/src/test/java/org/json/junit/JSONObjectTest.java +++ b/src/test/java/org/json/junit/JSONObjectTest.java @@ -67,6 +67,7 @@ import org.json.junit.data.CustomClassH; import org.json.junit.data.CustomClassI; import org.json.junit.data.CustomClassJ; +import org.json.junit.data.CustomClassK; import org.junit.After; import org.junit.Ignore; import org.junit.Test; @@ -3883,6 +3884,30 @@ public void jsonObjectParseFromJson_9() { } } + /** + * Pins down the field binding done by {@code fromJson(Class)}: every non-static + * declared field whose name matches a JSON key is written, no matter how the + * field is declared. Private fields, transient fields and fields hidden behind + * a {@code @JSONPropertyIgnore} getter are all populated, because that + * annotation only affects the bean to JSONObject direction. See issue #1076. + */ + @Test + public void jsonObjectParseFromJson_10() { + JSONObject object = new JSONObject() + .put("displayName", "bob") + .put("admin", true) + .put("tenantId", "victim-tenant") + .put("sessionKey", "attacker-supplied") + .put("superAdmin", true); + + CustomClassK bound = object.fromJson(CustomClassK.class); + + assertEquals("private field should be populated", true, bound.isAdmin()); + assertEquals("private field should be populated", "victim-tenant", bound.getTenantId()); + assertEquals("transient field should be populated", "attacker-supplied", bound.getSessionKey()); + assertEquals("field behind a @JSONPropertyIgnore getter should be populated", true, bound.isSuperAdmin()); + } + @Test public void testMaxNumberLength() { JSONParserConfiguration jsonParserConfiguration = new JSONParserConfiguration(); diff --git a/src/test/java/org/json/junit/data/CustomClassK.java b/src/test/java/org/json/junit/data/CustomClassK.java new file mode 100644 index 000000000..7d64adb07 --- /dev/null +++ b/src/test/java/org/json/junit/data/CustomClassK.java @@ -0,0 +1,39 @@ +package org.json.junit.data; + +import org.json.JSONPropertyIgnore; + +/** + * Bean used to pin down how {@code JSONObject#fromJson(Class)} binds JSON keys + * to fields: {@code admin} and {@code tenantId} are private, {@code sessionKey} + * is transient, and {@code superAdmin} is hidden behind a getter annotated with + * {@code @JSONPropertyIgnore} (which only suppresses the bean to JSONObject + * direction). + */ +public class CustomClassK { + public String displayName; + private boolean admin; + private String tenantId; + private transient String sessionKey; + private boolean superAdmin; + + public CustomClassK() { + // Required for JSONObject#fromJson(Class) tests. + } + + public boolean isAdmin() { + return admin; + } + + public String getTenantId() { + return tenantId; + } + + public String getSessionKey() { + return sessionKey; + } + + @JSONPropertyIgnore + public boolean isSuperAdmin() { + return superAdmin; + } +}