Skip to content

Commit 4bfce4d

Browse files
committed
1003: Implement JSONObject.fromJson() with unit tests
1 parent 9b8eefc commit 4bfce4d

File tree

3 files changed

+484
-0
lines changed

3 files changed

+484
-0
lines changed
Lines changed: 122 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,122 @@
1+
package org.json;
2+
3+
import java.util.Map;
4+
import java.util.HashMap;
5+
import java.util.List;
6+
import java.util.ArrayList;
7+
import java.util.Set;
8+
import java.util.HashSet;
9+
import java.util.Collection;
10+
import java.util.function.Function;
11+
import java.util.function.Supplier;
12+
13+
/**
14+
* The {@code JSONBuilder} class provides a configurable mechanism for
15+
* defining how different Java types are handled during JSON serialization
16+
* or deserialization.
17+
*
18+
* <p>This class maintains two internal mappings:
19+
* <ul>
20+
* <li>A {@code classMapping} which maps Java classes to functions that convert
21+
* an input {@code Object} into an appropriate instance of that class.</li>
22+
* <li>A {@code collectionMapping} which maps collection interfaces (like {@code List}, {@code Set}, {@code Map})
23+
* to supplier functions that create new instances of concrete implementations (e.g., {@code ArrayList} for {@code List}).</li>
24+
* </ul>
25+
*
26+
* <p>The mappings are initialized with default values for common primitive wrapper types
27+
* and collection interfaces, but they can be modified at runtime using setter methods.
28+
*
29+
* <p>This class is useful in custom JSON serialization/deserialization frameworks where
30+
* type transformation and collection instantiation logic needs to be flexible and extensible.
31+
*/
32+
public class JSONBuilder {
33+
34+
/**
35+
* A mapping from Java classes to functions that convert a generic {@code Object}
36+
* into an instance of the target class.
37+
*
38+
* <p>Examples of default mappings:
39+
* <ul>
40+
* <li>{@code int.class} or {@code Integer.class} -> Converts a {@code Number} to {@code int}</li>
41+
* <li>{@code boolean.class} or {@code Boolean.class} -> Identity function</li>
42+
* <li>{@code String.class} -> Identity function</li>
43+
* </ul>
44+
*/
45+
private static final Map<Class<?>, Function<Object, ?>> classMapping = new HashMap<>();
46+
47+
/**
48+
* A mapping from collection interface types to suppliers that produce
49+
* instances of concrete collection implementations.
50+
*
51+
* <p>Examples of default mappings:
52+
* <ul>
53+
* <li>{@code List.class} -> {@code ArrayList::new}</li>
54+
* <li>{@code Set.class} -> {@code HashSet::new}</li>
55+
* <li>{@code Map.class} -> {@code HashMap::new}</li>
56+
* </ul>
57+
*/
58+
private static final Map<Class<?>, Supplier<?>> collectionMapping = new HashMap<>();
59+
60+
// Static initializer block to populate default mappings
61+
static {
62+
classMapping.put(int.class, s -> ((Number) s).intValue());
63+
classMapping.put(Integer.class, s -> ((Number) s).intValue());
64+
classMapping.put(double.class, s -> ((Number) s).doubleValue());
65+
classMapping.put(Double.class, s -> ((Number) s).doubleValue());
66+
classMapping.put(float.class, s -> ((Number) s).floatValue());
67+
classMapping.put(Float.class, s -> ((Number) s).floatValue());
68+
classMapping.put(long.class, s -> ((Number) s).longValue());
69+
classMapping.put(Long.class, s -> ((Number) s).longValue());
70+
classMapping.put(boolean.class, s -> s);
71+
classMapping.put(Boolean.class, s -> s);
72+
classMapping.put(String.class, s -> s);
73+
74+
collectionMapping.put(List.class, ArrayList::new);
75+
collectionMapping.put(Set.class, HashSet::new);
76+
collectionMapping.put(Map.class, HashMap::new);
77+
}
78+
79+
/**
80+
* Returns the current class-to-function mapping used for type conversions.
81+
*
82+
* @return a map of classes to functions that convert an {@code Object} to that class
83+
*/
84+
public Map<Class<?>, Function<Object, ?>> getClassMapping() {
85+
return this.classMapping;
86+
}
87+
88+
/**
89+
* Returns the current collection-to-supplier mapping used for instantiating collections.
90+
*
91+
* @return a map of collection interface types to suppliers of concrete implementations
92+
*/
93+
public Map<Class<?>, Supplier<?>> getCollectionMapping() {
94+
return this.collectionMapping;
95+
}
96+
97+
/**
98+
* Adds or updates a type conversion function for a given class.
99+
*
100+
* <p>This allows users to customize how objects are converted into specific types
101+
* during processing (e.g., JSON deserialization).
102+
*
103+
* @param clazz the target class for which the conversion function is to be set
104+
* @param function a function that takes an {@code Object} and returns an instance of {@code clazz}
105+
*/
106+
public void setClassMapping(Class<?> clazz, Function<Object, ?> function) {
107+
classMapping.put(clazz, function);
108+
}
109+
110+
/**
111+
* Adds or updates a supplier function for instantiating a collection type.
112+
*
113+
* <p>This allows customization of which concrete implementation is used for
114+
* interface types like {@code List}, {@code Set}, or {@code Map}.
115+
*
116+
* @param clazz the collection interface class (e.g., {@code List.class})
117+
* @param function a supplier that creates a new instance of a concrete implementation
118+
*/
119+
public void setCollectionMapping(Class<?> clazz, Supplier<?> function) {
120+
collectionMapping.put(clazz, function);
121+
}
122+
}

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

Lines changed: 146 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,10 @@
1717
import java.util.*;
1818
import java.util.Map.Entry;
1919
import java.util.regex.Pattern;
20+
import java.util.function.Function;
21+
import java.util.function.Supplier;
22+
import java.lang.reflect.ParameterizedType;
23+
import java.lang.reflect.Type;
2024

2125
/**
2226
* A JSONObject is an unordered collection of name/value pairs. Its external
@@ -119,6 +123,12 @@ public String toString() {
119123
*/
120124
static final Pattern NUMBER_PATTERN = Pattern.compile("-?(?:0|[1-9]\\d*)(?:\\.\\d+)?(?:[eE][+-]?\\d+)?");
121125

126+
127+
/**
128+
* A Builder class for handling the conversion of JSON to Object.
129+
*/
130+
private JSONBuilder builder;
131+
122132
/**
123133
* The map where the JSONObject's properties are kept.
124134
*/
@@ -212,6 +222,25 @@ public JSONObject(JSONTokener x, JSONParserConfiguration jsonParserConfiguration
212222
}
213223
}
214224

225+
/**
226+
* Construct a JSONObject with JSONBuilder for conversion from JSON to POJO
227+
*
228+
* @param builder builder option for json to POJO
229+
*/
230+
public JSONObject(JSONBuilder builder) {
231+
this();
232+
this.builder = builder;
233+
}
234+
235+
/**
236+
* Method to set JSONBuilder.
237+
*
238+
* @param builder
239+
*/
240+
public void setJSONBuilder(JSONBuilder builder) {
241+
this.builder = builder;
242+
}
243+
215244
/**
216245
* Parses entirety of JSON object
217246
*
@@ -3110,4 +3139,121 @@ private static JSONException recursivelyDefinedObjectException(String key) {
31103139
"JavaBean object contains recursively defined member variable of key " + quote(key)
31113140
);
31123141
}
3142+
3143+
/**
3144+
* Deserializes a JSON string into an instance of the specified class.
3145+
*
3146+
* <p>This method attempts to map JSON key-value pairs to the corresponding fields
3147+
* of the given class. It supports basic data types including int, double, float,
3148+
* long, and boolean (as well as their boxed counterparts). The class must have a
3149+
* no-argument constructor, and the field names in the class must match the keys
3150+
* in the JSON string.
3151+
*
3152+
* @param clazz the class of the object to be returned
3153+
* @param <T> the type of the object
3154+
* @return an instance of type T with fields populated from the JSON string
3155+
*/
3156+
public <T> T fromJson(Class<T> clazz) {
3157+
try {
3158+
T obj = clazz.getDeclaredConstructor().newInstance();
3159+
if (this.builder == null) {
3160+
this.builder = new JSONBuilder();
3161+
}
3162+
Map<Class<?>, Function<Object, ?>> classMapping = this.builder.getClassMapping();
3163+
3164+
for (Field field: clazz.getDeclaredFields()) {
3165+
field.setAccessible(true);
3166+
String fieldName = field.getName();
3167+
if (this.has(fieldName)) {
3168+
Object value = this.get(fieldName);
3169+
Class<?> pojoClass = field.getType();
3170+
if (classMapping.containsKey(pojoClass)) {
3171+
field.set(obj, classMapping.get(pojoClass).apply(value));
3172+
} else {
3173+
if (value.getClass() == JSONObject.class) {
3174+
field.set(obj, fromJson((JSONObject) value, pojoClass));
3175+
} else if (value.getClass() == JSONArray.class) {
3176+
if (Collection.class.isAssignableFrom(pojoClass)) {
3177+
3178+
Collection<?> nestedCollection = fromJsonArray((JSONArray) value,
3179+
(Class<? extends Collection>) pojoClass,
3180+
field.getGenericType());
3181+
3182+
field.set(obj, nestedCollection);
3183+
}
3184+
}
3185+
}
3186+
}
3187+
}
3188+
return obj;
3189+
} catch (NoSuchMethodException | InstantiationException | IllegalAccessException | InvocationTargetException e) {
3190+
throw new JSONException(e);
3191+
}
3192+
}
3193+
3194+
private <T> Collection<T> fromJsonArray(JSONArray jsonArray, Class<?> collectionType, Type elementType) throws JSONException {
3195+
try {
3196+
Map<Class<?>, Function<Object, ?>> classMapping = this.builder.getClassMapping();
3197+
Map<Class<?>, Supplier<?>> collectionMapping = this.builder.getCollectionMapping();
3198+
Collection<T> collection = (Collection<T>) (collectionMapping.containsKey(collectionType) ?
3199+
collectionMapping.get(collectionType).get()
3200+
: collectionType.getDeclaredConstructor().newInstance());
3201+
3202+
3203+
Class<?> innerElementClass = null;
3204+
Type innerElementType = null;
3205+
if (elementType instanceof ParameterizedType) {
3206+
ParameterizedType pType = (ParameterizedType) elementType;
3207+
innerElementType = pType.getActualTypeArguments()[0];
3208+
innerElementClass = (innerElementType instanceof Class) ?
3209+
(Class<?>) innerElementType
3210+
: (Class<?>) ((ParameterizedType) innerElementType).getRawType();
3211+
} else {
3212+
innerElementClass = (Class<?>) elementType;
3213+
}
3214+
3215+
for (int i = 0; i < jsonArray.length(); i++) {
3216+
Object jsonElement = jsonArray.get(i);
3217+
if (classMapping.containsKey(innerElementClass)) {
3218+
collection.add((T) classMapping.get(innerElementClass).apply(jsonElement));
3219+
} else if (jsonElement.getClass() == JSONObject.class) {
3220+
collection.add((T) ((JSONObject) jsonElement).fromJson(innerElementClass));
3221+
} else if (jsonElement.getClass() == JSONArray.class) {
3222+
if (Collection.class.isAssignableFrom(innerElementClass)) {
3223+
3224+
Collection<?> nestedCollection = fromJsonArray((JSONArray) jsonElement,
3225+
innerElementClass,
3226+
innerElementType);
3227+
3228+
collection.add((T) nestedCollection);
3229+
} else {
3230+
throw new JSONException("Expected collection type for nested JSONArray, but got: " + innerElementClass);
3231+
}
3232+
} else {
3233+
collection.add((T) jsonElement.toString());
3234+
}
3235+
}
3236+
return collection;
3237+
} catch (NoSuchMethodException | InstantiationException | IllegalAccessException | InvocationTargetException e) {
3238+
throw new JSONException(e);
3239+
}
3240+
}
3241+
3242+
/**
3243+
* Deserializes a JSON string into an instance of the specified class.
3244+
*
3245+
* <p>This method attempts to map JSON key-value pairs to the corresponding fields
3246+
* of the given class. It supports basic data types including int, double, float,
3247+
* long, and boolean (as well as their boxed counterparts). The class must have a
3248+
* no-argument constructor, and the field names in the class must match the keys
3249+
* in the JSON string.
3250+
*
3251+
* @param object JSONObject of internal class
3252+
* @param clazz the class of the object to be returned
3253+
* @param <T> the type of the object
3254+
* @return an instance of type T with fields populated from the JSON string
3255+
*/
3256+
private <T> T fromJson(JSONObject object, Class<T> clazz) {
3257+
return object.fromJson(clazz);
3258+
}
31133259
}

0 commit comments

Comments
 (0)