Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
20 changes: 14 additions & 6 deletions src/main/java/org/json/JSONObject.java
Original file line number Diff line number Diff line change
Expand Up @@ -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 <T> 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> T fromJson(String jsonString, Class<T> clazz) {
JSONObject jsonObject = new JSONObject(jsonString);
Expand All @@ -3493,15 +3496,20 @@ public static <T> T fromJson(String jsonString, Class<T> 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.
*
* <p><strong>Note:</strong> 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.
* <p><strong>Note:</strong> 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 <T> 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> T fromJson(Class<T> clazz) {
Expand Down
25 changes: 25 additions & 0 deletions src/test/java/org/json/junit/JSONObjectTest.java
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -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();
Expand Down
39 changes: 39 additions & 0 deletions src/test/java/org/json/junit/data/CustomClassK.java
Original file line number Diff line number Diff line change
@@ -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<T>) tests.
}

public boolean isAdmin() {
return admin;
}

public String getTenantId() {
return tenantId;
}

public String getSessionKey() {
return sessionKey;
}

@JSONPropertyIgnore
public boolean isSuperAdmin() {
return superAdmin;
}
}