Skip to content

Commit 88b6be9

Browse files
committed
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
1 parent 69383f7 commit 88b6be9

3 files changed

Lines changed: 79 additions & 6 deletions

File tree

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

Lines changed: 14 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -3472,12 +3472,15 @@ private Type[] getMapTypes(Type type) {
34723472
* of the given class. It supports basic data types including int, double, float,
34733473
* long, and boolean (as well as their boxed counterparts). The class must have a
34743474
* no-argument constructor, and the field names in the class must match the keys
3475-
* in the JSON string.
3475+
* in the JSON string. Static fields are ignored; see {@link #fromJson(Class)} for
3476+
* how the remaining fields are bound.
34763477
*
34773478
* @param <T> the type of the object to return
34783479
* @param jsonString json in string format
34793480
* @param clazz the class of the object to be returned
34803481
* @return an instance of Object T with fields populated from the JSON string
3482+
* @throws JSONException if jsonString is not valid JSON, or if it cannot be bound to clazz
3483+
* as described in {@link #fromJson(Class)}
34813484
*/
34823485
public static <T> T fromJson(String jsonString, Class<T> clazz) {
34833486
JSONObject jsonObject = new JSONObject(jsonString);
@@ -3493,15 +3496,20 @@ public static <T> T fromJson(String jsonString, Class<T> clazz) {
34933496
* The target class must have a no-argument constructor, and its field names must match
34943497
* the keys in the JSON string. Static fields are ignored.
34953498
*
3496-
* <p><strong>Note:</strong> Only classes that are explicitly supported and registered within
3497-
* the {@code JSONObject} context can be deserialized. If the provided class is not among those,
3498-
* this method will not be able to deserialize it. This ensures that only a limited and
3499-
* controlled set of types can be instantiated from JSON for safety and predictability.
3499+
* <p><strong>Note:</strong> binding is driven by the keys present in the JSON, not by
3500+
* how the fields are declared. Every non-static field declared by the class is written
3501+
* when its name matches a key, including {@code private} and {@code transient} fields,
3502+
* and field access is widened with
3503+
* {@link java.lang.reflect.Field#setAccessible(boolean)}. {@link JSONPropertyIgnore}
3504+
* only suppresses the bean to JSONObject direction, so it does not keep a field from
3505+
* being populated here. JSON coming from an untrusted source should therefore be
3506+
* validated, or reduced to the expected keys, before it is handed to this method.
35003507
*
35013508
* @param clazz the class of the object to be returned
35023509
* @param <T> the type of the object
35033510
* @return an instance of type {@code T} with fields populated from the JSON string
3504-
* @throws IllegalArgumentException if the class is not supported for deserialization
3511+
* @throws JSONException if the class has no accessible no-argument constructor, or if a
3512+
* field cannot be instantiated or set
35053513
*/
35063514
@SuppressWarnings("unchecked")
35073515
public <T> T fromJson(Class<T> clazz) {

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

Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -67,6 +67,7 @@
6767
import org.json.junit.data.CustomClassH;
6868
import org.json.junit.data.CustomClassI;
6969
import org.json.junit.data.CustomClassJ;
70+
import org.json.junit.data.FromJsonKeyBindingExample;
7071
import org.junit.After;
7172
import org.junit.Ignore;
7273
import org.junit.Test;
@@ -3883,6 +3884,31 @@ public void jsonObjectParseFromJson_9() {
38833884
}
38843885
}
38853886

3887+
/**
3888+
* Pins down the field binding done by {@code fromJson(Class)}: every non-static
3889+
* declared field whose name matches a JSON key is written, no matter how the
3890+
* field is declared. Private fields, transient fields and fields hidden behind
3891+
* a {@code @JSONPropertyIgnore} getter are all populated, because that
3892+
* annotation only affects the bean to JSONObject direction. See issue #1076.
3893+
*/
3894+
@Test
3895+
public void jsonObjectParseFromJson_10() {
3896+
JSONObject object = new JSONObject()
3897+
.put("displayName", "bob")
3898+
.put("privateBoolean", true)
3899+
.put("privateString", "private-value")
3900+
.put("transientString", "transient-value")
3901+
.put("ignoredGetterBoolean", true);
3902+
3903+
FromJsonKeyBindingExample bound = object.fromJson(FromJsonKeyBindingExample.class);
3904+
3905+
assertEquals("private field should be populated", true, bound.isPrivateBoolean());
3906+
assertEquals("private field should be populated", "private-value", bound.getPrivateString());
3907+
assertEquals("transient field should be populated", "transient-value", bound.getTransientString());
3908+
assertEquals("field behind a @JSONPropertyIgnore getter should be populated", true,
3909+
bound.isIgnoredGetterBoolean());
3910+
}
3911+
38863912
@Test
38873913
public void testMaxNumberLength() {
38883914
JSONParserConfiguration jsonParserConfiguration = new JSONParserConfiguration();
Lines changed: 39 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,39 @@
1+
package org.json.junit.data;
2+
3+
import org.json.JSONPropertyIgnore;
4+
5+
/**
6+
* Bean used to pin down how {@code JSONObject#fromJson(Class)} binds JSON keys
7+
* to fields: {@code privateString} and {@code privateBoolean} are private,
8+
* {@code transientString} is transient, and {@code ignoredGetterBoolean} is
9+
* hidden behind a getter annotated with {@code @JSONPropertyIgnore} (which only
10+
* suppresses the bean to JSONObject direction).
11+
*/
12+
public class FromJsonKeyBindingExample {
13+
public String displayName;
14+
private boolean privateBoolean;
15+
private String privateString;
16+
private transient String transientString;
17+
private boolean ignoredGetterBoolean;
18+
19+
public FromJsonKeyBindingExample() {
20+
// Required for JSONObject#fromJson(Class<T>) tests.
21+
}
22+
23+
public boolean isPrivateBoolean() {
24+
return privateBoolean;
25+
}
26+
27+
public String getPrivateString() {
28+
return privateString;
29+
}
30+
31+
public String getTransientString() {
32+
return transientString;
33+
}
34+
35+
@JSONPropertyIgnore
36+
public boolean isIgnoredGetterBoolean() {
37+
return ignoredGetterBoolean;
38+
}
39+
}

0 commit comments

Comments
 (0)