Skip to content

Commit 25f355a

Browse files
authored
Merge pull request #1006 from sk02241994/feature-1003
1003: Implement JSONObject.fromJson() with unit tests
2 parents 3e8d1d1 + 42800c2 commit 25f355a

File tree

12 files changed

+588
-0
lines changed

12 files changed

+588
-0
lines changed

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

Lines changed: 249 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,9 @@
1717
import java.util.*;
1818
import java.util.Map.Entry;
1919
import java.util.regex.Pattern;
20+
import java.lang.reflect.ParameterizedType;
21+
import java.lang.reflect.Type;
22+
import java.lang.reflect.GenericArrayType;
2023

2124
/**
2225
* A JSONObject is an unordered collection of name/value pairs. Its external
@@ -3207,4 +3210,250 @@ private static JSONException recursivelyDefinedObjectException(String key) {
32073210
"JavaBean object contains recursively defined member variable of key " + quote(key)
32083211
);
32093212
}
3213+
3214+
/**
3215+
* Helper method to extract the raw Class from Type.
3216+
*/
3217+
private Class<?> getRawType(Type type) {
3218+
if (type instanceof Class) {
3219+
return (Class<?>) type;
3220+
} else if (type instanceof ParameterizedType) {
3221+
return (Class<?>) ((ParameterizedType) type).getRawType();
3222+
} else if (type instanceof GenericArrayType) {
3223+
return Object[].class; // Simplified handling for arrays
3224+
}
3225+
return Object.class; // Fallback
3226+
}
3227+
3228+
/**
3229+
* Extracts the element Type for a Collection Type.
3230+
*/
3231+
private Type getElementType(Type type) {
3232+
if (type instanceof ParameterizedType) {
3233+
Type[] args = ((ParameterizedType) type).getActualTypeArguments();
3234+
return args.length > 0 ? args[0] : Object.class;
3235+
}
3236+
return Object.class;
3237+
}
3238+
3239+
/**
3240+
* Extracts the key and value Types for a Map Type.
3241+
*/
3242+
private Type[] getMapTypes(Type type) {
3243+
if (type instanceof ParameterizedType) {
3244+
Type[] args = ((ParameterizedType) type).getActualTypeArguments();
3245+
if (args.length == 2) {
3246+
return args;
3247+
}
3248+
}
3249+
return new Type[]{Object.class, Object.class}; // Default: String keys, Object values
3250+
}
3251+
3252+
/**
3253+
* Deserializes a JSON string into an instance of the specified class.
3254+
*
3255+
* <p>This method attempts to map JSON key-value pairs to the corresponding fields
3256+
* of the given class. It supports basic data types including int, double, float,
3257+
* long, and boolean (as well as their boxed counterparts). The class must have a
3258+
* no-argument constructor, and the field names in the class must match the keys
3259+
* in the JSON string.
3260+
*
3261+
* @param jsonString json in string format
3262+
* @param clazz the class of the object to be returned
3263+
* @return an instance of Object T with fields populated from the JSON string
3264+
*/
3265+
public static <T> T fromJson(String jsonString, Class<T> clazz) {
3266+
JSONObject jsonObject = new JSONObject(jsonString);
3267+
return jsonObject.fromJson(clazz);
3268+
}
3269+
3270+
/**
3271+
* Deserializes a JSON string into an instance of the specified class.
3272+
*
3273+
* <p>This method attempts to map JSON key-value pairs to the corresponding fields
3274+
* of the given class. It supports basic data types including {@code int}, {@code double},
3275+
* {@code float}, {@code long}, and {@code boolean}, as well as their boxed counterparts.
3276+
* The target class must have a no-argument constructor, and its field names must match
3277+
* the keys in the JSON string.
3278+
*
3279+
* <p><strong>Note:</strong> Only classes that are explicitly supported and registered within
3280+
* the {@code JSONObject} context can be deserialized. If the provided class is not among those,
3281+
* this method will not be able to deserialize it. This ensures that only a limited and
3282+
* controlled set of types can be instantiated from JSON for safety and predictability.
3283+
*
3284+
* @param clazz the class of the object to be returned
3285+
* @param <T> the type of the object
3286+
* @return an instance of type {@code T} with fields populated from the JSON string
3287+
* @throws IllegalArgumentException if the class is not supported for deserialization
3288+
*/
3289+
@SuppressWarnings("unchecked")
3290+
public <T> T fromJson(Class<T> clazz) {
3291+
try {
3292+
T obj = clazz.getDeclaredConstructor().newInstance();
3293+
for (Field field : clazz.getDeclaredFields()) {
3294+
field.setAccessible(true);
3295+
String fieldName = field.getName();
3296+
if (has(fieldName)) {
3297+
Object value = get(fieldName);
3298+
Type fieldType = field.getGenericType();
3299+
Object convertedValue = convertValue(value, fieldType);
3300+
field.set(obj, convertedValue);
3301+
}
3302+
}
3303+
return obj;
3304+
} catch (NoSuchMethodException e) {
3305+
throw new JSONException("No no-arg constructor for class: " + clazz.getName(), e);
3306+
} catch (Exception e) {
3307+
throw new JSONException("Failed to instantiate or set field for class: " + clazz.getName(), e);
3308+
}
3309+
}
3310+
3311+
/**
3312+
* Recursively converts a value to the target Type, handling nested generics for Collections and Maps.
3313+
*/
3314+
private Object convertValue(Object value, Type targetType) throws JSONException {
3315+
if (value == null) {
3316+
return null;
3317+
}
3318+
3319+
Class<?> rawType = getRawType(targetType);
3320+
3321+
// Direct assignment
3322+
if (rawType.isAssignableFrom(value.getClass())) {
3323+
return value;
3324+
}
3325+
3326+
if (rawType == int.class || rawType == Integer.class) {
3327+
return ((Number) value).intValue();
3328+
} else if (rawType == double.class || rawType == Double.class) {
3329+
return ((Number) value).doubleValue();
3330+
} else if (rawType == float.class || rawType == Float.class) {
3331+
return ((Number) value).floatValue();
3332+
} else if (rawType == long.class || rawType == Long.class) {
3333+
return ((Number) value).longValue();
3334+
} else if (rawType == boolean.class || rawType == Boolean.class) {
3335+
return value;
3336+
} else if (rawType == String.class) {
3337+
return value;
3338+
} else if (rawType == BigDecimal.class) {
3339+
return new BigDecimal((String) value);
3340+
} else if (rawType == BigInteger.class) {
3341+
return new BigInteger((String) value);
3342+
}
3343+
3344+
// Enum conversion
3345+
if (rawType.isEnum() && value instanceof String) {
3346+
return stringToEnum(rawType, (String) value);
3347+
}
3348+
3349+
// Collection handling (e.g., List<List<Map<String, Integer>>>)
3350+
if (Collection.class.isAssignableFrom(rawType)) {
3351+
if (value instanceof JSONArray) {
3352+
Type elementType = getElementType(targetType);
3353+
return fromJsonArray((JSONArray) value, rawType, elementType);
3354+
}
3355+
}
3356+
// Map handling (e.g., Map<Integer, List<String>>)
3357+
else if (Map.class.isAssignableFrom(rawType) && value instanceof JSONObject) {
3358+
Type[] mapTypes = getMapTypes(targetType);
3359+
Type keyType = mapTypes[0];
3360+
Type valueType = mapTypes[1];
3361+
return convertToMap((JSONObject) value, keyType, valueType, rawType);
3362+
}
3363+
// POJO handling (including custom classes like Tuple<Integer, String, Integer>)
3364+
else if (!rawType.isPrimitive() && !rawType.isEnum() && value instanceof JSONObject) {
3365+
// Recurse with the raw class for POJO deserialization
3366+
return ((JSONObject) value).fromJson(rawType);
3367+
}
3368+
3369+
// Fallback
3370+
return value.toString();
3371+
}
3372+
3373+
/**
3374+
* Converts a JSONObject to a Map with the specified generic key and value Types.
3375+
* Supports nested types via recursive convertValue.
3376+
*/
3377+
private Map<?, ?> convertToMap(JSONObject jsonMap, Type keyType, Type valueType, Class<?> mapType) throws JSONException {
3378+
try {
3379+
@SuppressWarnings("unchecked")
3380+
Map<Object, Object> createdMap = new HashMap();
3381+
3382+
for (Object keyObj : jsonMap.keySet()) {
3383+
String keyStr = (String) keyObj;
3384+
Object mapValue = jsonMap.get(keyStr);
3385+
// Convert key (e.g., String to Integer for Map<Integer, ...>)
3386+
Object convertedKey = convertValue(keyStr, keyType);
3387+
// Convert value recursively (handles nesting)
3388+
Object convertedValue = convertValue(mapValue, valueType);
3389+
createdMap.put(convertedKey, convertedValue);
3390+
}
3391+
return createdMap;
3392+
} catch (Exception e) {
3393+
throw new JSONException("Failed to convert JSONObject to Map: " + mapType.getName(), e);
3394+
}
3395+
}
3396+
3397+
/**
3398+
* Converts a String to an Enum value.
3399+
*/
3400+
private <E> E stringToEnum(Class<?> enumClass, String value) throws JSONException {
3401+
try {
3402+
@SuppressWarnings("unchecked")
3403+
Class<E> enumType = (Class<E>) enumClass;
3404+
Method valueOfMethod = enumType.getMethod("valueOf", String.class);
3405+
return (E) valueOfMethod.invoke(null, value);
3406+
} catch (Exception e) {
3407+
throw new JSONException("Failed to convert string to enum: " + value + " for " + enumClass.getName(), e);
3408+
}
3409+
}
3410+
3411+
/**
3412+
* Deserializes a JSONArray into a Collection, supporting nested generics.
3413+
* Uses recursive convertValue for elements.
3414+
*/
3415+
@SuppressWarnings("unchecked")
3416+
private <T> Collection<T> fromJsonArray(JSONArray jsonArray, Class<?> collectionType, Type elementType) throws JSONException {
3417+
try {
3418+
Collection<T> collection = getCollection(collectionType);
3419+
3420+
for (int i = 0; i < jsonArray.length(); i++) {
3421+
Object jsonElement = jsonArray.get(i);
3422+
// Recursively convert each element using the full element Type (handles nesting)
3423+
Object convertedValue = convertValue(jsonElement, elementType);
3424+
collection.add((T) convertedValue);
3425+
}
3426+
return collection;
3427+
} catch (Exception e) {
3428+
throw new JSONException("Failed to convert JSONArray to Collection: " + collectionType.getName(), e);
3429+
}
3430+
}
3431+
3432+
/**
3433+
* Creates and returns a new instance of a supported {@link Collection} implementation
3434+
* based on the specified collection type.
3435+
* <p>
3436+
* This method currently supports the following collection types:
3437+
* <ul>
3438+
* <li>{@code List.class}</li>
3439+
* <li>{@code ArrayList.class}</li>
3440+
* <li>{@code Set.class}</li>
3441+
* <li>{@code HashSet.class}</li>
3442+
* </ul>
3443+
* If the provided type does not match any of the supported types, a {@link JSONException}
3444+
* is thrown.
3445+
*
3446+
* @param collectionType the {@link Class} object representing the desired collection type
3447+
* @return a new empty instance of the specified collection type
3448+
* @throws JSONException if the specified type is not a supported collection type
3449+
*/
3450+
private Collection getCollection(Class<?> collectionType) throws JSONException {
3451+
if (collectionType == List.class || collectionType == ArrayList.class) {
3452+
return new ArrayList();
3453+
} else if (collectionType == Set.class || collectionType == HashSet.class) {
3454+
return new HashSet();
3455+
} else {
3456+
throw new JSONException("Unsupported Collection type: " + collectionType.getName());
3457+
}
3458+
}
32103459
}

0 commit comments

Comments
 (0)