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; + } +}