Skip to content

Commit 83a0e34

Browse files
committed
1003: Implement JSONObject.fromJson() with unit tests
1 parent 3e8d1d1 commit 83a0e34

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
*
@@ -3207,4 +3236,121 @@ private static JSONException recursivelyDefinedObjectException(String key) {
32073236
"JavaBean object contains recursively defined member variable of key " + quote(key)
32083237
);
32093238
}
3239+
3240+
/**
3241+
* Deserializes a JSON string into an instance of the specified class.
3242+
*
3243+
* <p>This method attempts to map JSON key-value pairs to the corresponding fields
3244+
* of the given class. It supports basic data types including int, double, float,
3245+
* long, and boolean (as well as their boxed counterparts). The class must have a
3246+
* no-argument constructor, and the field names in the class must match the keys
3247+
* in the JSON string.
3248+
*
3249+
* @param clazz the class of the object to be returned
3250+
* @param <T> the type of the object
3251+
* @return an instance of type T with fields populated from the JSON string
3252+
*/
3253+
public <T> T fromJson(Class<T> clazz) {
3254+
try {
3255+
T obj = clazz.getDeclaredConstructor().newInstance();
3256+
if (this.builder == null) {
3257+
this.builder = new JSONBuilder();
3258+
}
3259+
Map<Class<?>, Function<Object, ?>> classMapping = this.builder.getClassMapping();
3260+
3261+
for (Field field: clazz.getDeclaredFields()) {
3262+
field.setAccessible(true);
3263+
String fieldName = field.getName();
3264+
if (this.has(fieldName)) {
3265+
Object value = this.get(fieldName);
3266+
Class<?> pojoClass = field.getType();
3267+
if (classMapping.containsKey(pojoClass)) {
3268+
field.set(obj, classMapping.get(pojoClass).apply(value));
3269+
} else {
3270+
if (value.getClass() == JSONObject.class) {
3271+
field.set(obj, fromJson((JSONObject) value, pojoClass));
3272+
} else if (value.getClass() == JSONArray.class) {
3273+
if (Collection.class.isAssignableFrom(pojoClass)) {
3274+
3275+
Collection<?> nestedCollection = fromJsonArray((JSONArray) value,
3276+
(Class<? extends Collection>) pojoClass,
3277+
field.getGenericType());
3278+
3279+
field.set(obj, nestedCollection);
3280+
}
3281+
}
3282+
}
3283+
}
3284+
}
3285+
return obj;
3286+
} catch (NoSuchMethodException | InstantiationException | IllegalAccessException | InvocationTargetException e) {
3287+
throw new JSONException(e);
3288+
}
3289+
}
3290+
3291+
private <T> Collection<T> fromJsonArray(JSONArray jsonArray, Class<?> collectionType, Type elementType) throws JSONException {
3292+
try {
3293+
Map<Class<?>, Function<Object, ?>> classMapping = this.builder.getClassMapping();
3294+
Map<Class<?>, Supplier<?>> collectionMapping = this.builder.getCollectionMapping();
3295+
Collection<T> collection = (Collection<T>) (collectionMapping.containsKey(collectionType) ?
3296+
collectionMapping.get(collectionType).get()
3297+
: collectionType.getDeclaredConstructor().newInstance());
3298+
3299+
3300+
Class<?> innerElementClass = null;
3301+
Type innerElementType = null;
3302+
if (elementType instanceof ParameterizedType) {
3303+
ParameterizedType pType = (ParameterizedType) elementType;
3304+
innerElementType = pType.getActualTypeArguments()[0];
3305+
innerElementClass = (innerElementType instanceof Class) ?
3306+
(Class<?>) innerElementType
3307+
: (Class<?>) ((ParameterizedType) innerElementType).getRawType();
3308+
} else {
3309+
innerElementClass = (Class<?>) elementType;
3310+
}
3311+
3312+
for (int i = 0; i < jsonArray.length(); i++) {
3313+
Object jsonElement = jsonArray.get(i);
3314+
if (classMapping.containsKey(innerElementClass)) {
3315+
collection.add((T) classMapping.get(innerElementClass).apply(jsonElement));
3316+
} else if (jsonElement.getClass() == JSONObject.class) {
3317+
collection.add((T) ((JSONObject) jsonElement).fromJson(innerElementClass));
3318+
} else if (jsonElement.getClass() == JSONArray.class) {
3319+
if (Collection.class.isAssignableFrom(innerElementClass)) {
3320+
3321+
Collection<?> nestedCollection = fromJsonArray((JSONArray) jsonElement,
3322+
innerElementClass,
3323+
innerElementType);
3324+
3325+
collection.add((T) nestedCollection);
3326+
} else {
3327+
throw new JSONException("Expected collection type for nested JSONArray, but got: " + innerElementClass);
3328+
}
3329+
} else {
3330+
collection.add((T) jsonElement.toString());
3331+
}
3332+
}
3333+
return collection;
3334+
} catch (NoSuchMethodException | InstantiationException | IllegalAccessException | InvocationTargetException e) {
3335+
throw new JSONException(e);
3336+
}
3337+
}
3338+
3339+
/**
3340+
* Deserializes a JSON string into an instance of the specified class.
3341+
*
3342+
* <p>This method attempts to map JSON key-value pairs to the corresponding fields
3343+
* of the given class. It supports basic data types including int, double, float,
3344+
* long, and boolean (as well as their boxed counterparts). The class must have a
3345+
* no-argument constructor, and the field names in the class must match the keys
3346+
* in the JSON string.
3347+
*
3348+
* @param object JSONObject of internal class
3349+
* @param clazz the class of the object to be returned
3350+
* @param <T> the type of the object
3351+
* @return an instance of type T with fields populated from the JSON string
3352+
*/
3353+
private <T> T fromJson(JSONObject object, Class<T> clazz) {
3354+
return object.fromJson(clazz);
3355+
}
32103356
}

0 commit comments

Comments
 (0)