From 88b6be9f84cd39e38166950797fa0358f8bad2ff Mon Sep 17 00:00:00 2001 From: Lyin-OTTF Date: Tue, 20 Oct 2026 23:36:08 +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. The test bean is named FromJsonKeyBindingExample and its fields spell out how they are declared (privateString, privateBoolean, transientString, ignoredGetterBoolean), so the test reads as a binding example rather than as a scenario. Fixes #1076 --- src/main/java/org/json/JSONObject.java | 20 +++++++--- .../java/org/json/junit/JSONObjectTest.java | 26 +++++++++++++ .../junit/data/FromJsonKeyBindingExample.java | 39 +++++++++++++++++++ 3 files changed, 79 insertions(+), 6 deletions(-) create mode 100644 src/test/java/org/json/junit/data/FromJsonKeyBindingExample.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..ecc3204e7 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.FromJsonKeyBindingExample; import org.junit.After; import org.junit.Ignore; import org.junit.Test; @@ -3883,6 +3884,31 @@ 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("privateBoolean", true) + .put("privateString", "private-value") + .put("transientString", "transient-value") + .put("ignoredGetterBoolean", true); + + FromJsonKeyBindingExample bound = object.fromJson(FromJsonKeyBindingExample.class); + + assertEquals("private field should be populated", true, bound.isPrivateBoolean()); + assertEquals("private field should be populated", "private-value", bound.getPrivateString()); + assertEquals("transient field should be populated", "transient-value", bound.getTransientString()); + assertEquals("field behind a @JSONPropertyIgnore getter should be populated", true, + bound.isIgnoredGetterBoolean()); + } + @Test public void testMaxNumberLength() { JSONParserConfiguration jsonParserConfiguration = new JSONParserConfiguration(); diff --git a/src/test/java/org/json/junit/data/FromJsonKeyBindingExample.java b/src/test/java/org/json/junit/data/FromJsonKeyBindingExample.java new file mode 100644 index 000000000..5d281424f --- /dev/null +++ b/src/test/java/org/json/junit/data/FromJsonKeyBindingExample.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 privateString} and {@code privateBoolean} are private, + * {@code transientString} is transient, and {@code ignoredGetterBoolean} is + * hidden behind a getter annotated with {@code @JSONPropertyIgnore} (which only + * suppresses the bean to JSONObject direction). + */ +public class FromJsonKeyBindingExample { + public String displayName; + private boolean privateBoolean; + private String privateString; + private transient String transientString; + private boolean ignoredGetterBoolean; + + public FromJsonKeyBindingExample() { + // Required for JSONObject#fromJson(Class) tests. + } + + public boolean isPrivateBoolean() { + return privateBoolean; + } + + public String getPrivateString() { + return privateString; + } + + public String getTransientString() { + return transientString; + } + + @JSONPropertyIgnore + public boolean isIgnoredGetterBoolean() { + return ignoredGetterBoolean; + } +}