1003: Implement JSONObject.fromJson() with unit tests

This commit is contained in:
sk02241994
2025-09-09 15:05:34 +10:00
parent 3e8d1d119f
commit 83a0e34be5
3 changed files with 484 additions and 0 deletions

View File

@@ -0,0 +1,122 @@
package org.json;
import java.util.Map;
import java.util.HashMap;
import java.util.List;
import java.util.ArrayList;
import java.util.Set;
import java.util.HashSet;
import java.util.Collection;
import java.util.function.Function;
import java.util.function.Supplier;
/**
* The {@code JSONBuilder} class provides a configurable mechanism for
* defining how different Java types are handled during JSON serialization
* or deserialization.
*
* <p>This class maintains two internal mappings:
* <ul>
* <li>A {@code classMapping} which maps Java classes to functions that convert
* an input {@code Object} into an appropriate instance of that class.</li>
* <li>A {@code collectionMapping} which maps collection interfaces (like {@code List}, {@code Set}, {@code Map})
* to supplier functions that create new instances of concrete implementations (e.g., {@code ArrayList} for {@code List}).</li>
* </ul>
*
* <p>The mappings are initialized with default values for common primitive wrapper types
* and collection interfaces, but they can be modified at runtime using setter methods.
*
* <p>This class is useful in custom JSON serialization/deserialization frameworks where
* type transformation and collection instantiation logic needs to be flexible and extensible.
*/
public class JSONBuilder {
/**
* A mapping from Java classes to functions that convert a generic {@code Object}
* into an instance of the target class.
*
* <p>Examples of default mappings:
* <ul>
* <li>{@code int.class} or {@code Integer.class} -> Converts a {@code Number} to {@code int}</li>
* <li>{@code boolean.class} or {@code Boolean.class} -> Identity function</li>
* <li>{@code String.class} -> Identity function</li>
* </ul>
*/
private static final Map<Class<?>, Function<Object, ?>> classMapping = new HashMap<>();
/**
* A mapping from collection interface types to suppliers that produce
* instances of concrete collection implementations.
*
* <p>Examples of default mappings:
* <ul>
* <li>{@code List.class} -> {@code ArrayList::new}</li>
* <li>{@code Set.class} -> {@code HashSet::new}</li>
* <li>{@code Map.class} -> {@code HashMap::new}</li>
* </ul>
*/
private static final Map<Class<?>, Supplier<?>> collectionMapping = new HashMap<>();
// Static initializer block to populate default mappings
static {
classMapping.put(int.class, s -> ((Number) s).intValue());
classMapping.put(Integer.class, s -> ((Number) s).intValue());
classMapping.put(double.class, s -> ((Number) s).doubleValue());
classMapping.put(Double.class, s -> ((Number) s).doubleValue());
classMapping.put(float.class, s -> ((Number) s).floatValue());
classMapping.put(Float.class, s -> ((Number) s).floatValue());
classMapping.put(long.class, s -> ((Number) s).longValue());
classMapping.put(Long.class, s -> ((Number) s).longValue());
classMapping.put(boolean.class, s -> s);
classMapping.put(Boolean.class, s -> s);
classMapping.put(String.class, s -> s);
collectionMapping.put(List.class, ArrayList::new);
collectionMapping.put(Set.class, HashSet::new);
collectionMapping.put(Map.class, HashMap::new);
}
/**
* Returns the current class-to-function mapping used for type conversions.
*
* @return a map of classes to functions that convert an {@code Object} to that class
*/
public Map<Class<?>, Function<Object, ?>> getClassMapping() {
return this.classMapping;
}
/**
* Returns the current collection-to-supplier mapping used for instantiating collections.
*
* @return a map of collection interface types to suppliers of concrete implementations
*/
public Map<Class<?>, Supplier<?>> getCollectionMapping() {
return this.collectionMapping;
}
/**
* Adds or updates a type conversion function for a given class.
*
* <p>This allows users to customize how objects are converted into specific types
* during processing (e.g., JSON deserialization).
*
* @param clazz the target class for which the conversion function is to be set
* @param function a function that takes an {@code Object} and returns an instance of {@code clazz}
*/
public void setClassMapping(Class<?> clazz, Function<Object, ?> function) {
classMapping.put(clazz, function);
}
/**
* Adds or updates a supplier function for instantiating a collection type.
*
* <p>This allows customization of which concrete implementation is used for
* interface types like {@code List}, {@code Set}, or {@code Map}.
*
* @param clazz the collection interface class (e.g., {@code List.class})
* @param function a supplier that creates a new instance of a concrete implementation
*/
public void setCollectionMapping(Class<?> clazz, Supplier<?> function) {
collectionMapping.put(clazz, function);
}
}

View File

@@ -17,6 +17,10 @@ import java.math.BigInteger;
import java.util.*;
import java.util.Map.Entry;
import java.util.regex.Pattern;
import java.util.function.Function;
import java.util.function.Supplier;
import java.lang.reflect.ParameterizedType;
import java.lang.reflect.Type;
/**
* A JSONObject is an unordered collection of name/value pairs. Its external
@@ -119,6 +123,12 @@ public class JSONObject {
*/
static final Pattern NUMBER_PATTERN = Pattern.compile("-?(?:0|[1-9]\\d*)(?:\\.\\d+)?(?:[eE][+-]?\\d+)?");
/**
* A Builder class for handling the conversion of JSON to Object.
*/
private JSONBuilder builder;
/**
* The map where the JSONObject's properties are kept.
*/
@@ -212,6 +222,25 @@ public class JSONObject {
}
}
/**
* Construct a JSONObject with JSONBuilder for conversion from JSON to POJO
*
* @param builder builder option for json to POJO
*/
public JSONObject(JSONBuilder builder) {
this();
this.builder = builder;
}
/**
* Method to set JSONBuilder.
*
* @param builder
*/
public void setJSONBuilder(JSONBuilder builder) {
this.builder = builder;
}
/**
* Parses entirety of JSON object
*
@@ -3207,4 +3236,121 @@ public class JSONObject {
"JavaBean object contains recursively defined member variable of key " + quote(key)
);
}
/**
* Deserializes a JSON string into an instance of the specified class.
*
* <p>This method attempts to map JSON key-value pairs to the corresponding fields
* 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.
*
* @param clazz the class of the object to be returned
* @param <T> the type of the object
* @return an instance of type T with fields populated from the JSON string
*/
public <T> T fromJson(Class<T> clazz) {
try {
T obj = clazz.getDeclaredConstructor().newInstance();
if (this.builder == null) {
this.builder = new JSONBuilder();
}
Map<Class<?>, Function<Object, ?>> classMapping = this.builder.getClassMapping();
for (Field field: clazz.getDeclaredFields()) {
field.setAccessible(true);
String fieldName = field.getName();
if (this.has(fieldName)) {
Object value = this.get(fieldName);
Class<?> pojoClass = field.getType();
if (classMapping.containsKey(pojoClass)) {
field.set(obj, classMapping.get(pojoClass).apply(value));
} else {
if (value.getClass() == JSONObject.class) {
field.set(obj, fromJson((JSONObject) value, pojoClass));
} else if (value.getClass() == JSONArray.class) {
if (Collection.class.isAssignableFrom(pojoClass)) {
Collection<?> nestedCollection = fromJsonArray((JSONArray) value,
(Class<? extends Collection>) pojoClass,
field.getGenericType());
field.set(obj, nestedCollection);
}
}
}
}
}
return obj;
} catch (NoSuchMethodException | InstantiationException | IllegalAccessException | InvocationTargetException e) {
throw new JSONException(e);
}
}
private <T> Collection<T> fromJsonArray(JSONArray jsonArray, Class<?> collectionType, Type elementType) throws JSONException {
try {
Map<Class<?>, Function<Object, ?>> classMapping = this.builder.getClassMapping();
Map<Class<?>, Supplier<?>> collectionMapping = this.builder.getCollectionMapping();
Collection<T> collection = (Collection<T>) (collectionMapping.containsKey(collectionType) ?
collectionMapping.get(collectionType).get()
: collectionType.getDeclaredConstructor().newInstance());
Class<?> innerElementClass = null;
Type innerElementType = null;
if (elementType instanceof ParameterizedType) {
ParameterizedType pType = (ParameterizedType) elementType;
innerElementType = pType.getActualTypeArguments()[0];
innerElementClass = (innerElementType instanceof Class) ?
(Class<?>) innerElementType
: (Class<?>) ((ParameterizedType) innerElementType).getRawType();
} else {
innerElementClass = (Class<?>) elementType;
}
for (int i = 0; i < jsonArray.length(); i++) {
Object jsonElement = jsonArray.get(i);
if (classMapping.containsKey(innerElementClass)) {
collection.add((T) classMapping.get(innerElementClass).apply(jsonElement));
} else if (jsonElement.getClass() == JSONObject.class) {
collection.add((T) ((JSONObject) jsonElement).fromJson(innerElementClass));
} else if (jsonElement.getClass() == JSONArray.class) {
if (Collection.class.isAssignableFrom(innerElementClass)) {
Collection<?> nestedCollection = fromJsonArray((JSONArray) jsonElement,
innerElementClass,
innerElementType);
collection.add((T) nestedCollection);
} else {
throw new JSONException("Expected collection type for nested JSONArray, but got: " + innerElementClass);
}
} else {
collection.add((T) jsonElement.toString());
}
}
return collection;
} catch (NoSuchMethodException | InstantiationException | IllegalAccessException | InvocationTargetException e) {
throw new JSONException(e);
}
}
/**
* Deserializes a JSON string into an instance of the specified class.
*
* <p>This method attempts to map JSON key-value pairs to the corresponding fields
* 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.
*
* @param object JSONObject of internal class
* @param clazz the class of the object to be returned
* @param <T> the type of the object
* @return an instance of type T with fields populated from the JSON string
*/
private <T> T fromJson(JSONObject object, Class<T> clazz) {
return object.fromJson(clazz);
}
}

View File

@@ -33,6 +33,7 @@ import org.json.JSONObject;
import org.json.JSONPointerException;
import org.json.JSONParserConfiguration;
import org.json.JSONString;
import org.json.JSONBuilder;
import org.json.JSONTokener;
import org.json.ParserConfiguration;
import org.json.XML;
@@ -4095,4 +4096,219 @@ public class JSONObjectTest {
assertTrue("JSONObject should be empty", jsonObject.isEmpty());
}
@Test
public void jsonObjectParseFromJson_0() {
JSONObject object = new JSONObject();
object.put("number", 12);
object.put("name", "Alex");
object.put("longNumber", 1500000000L);
String jsonObject = object.toString();
CustomClass customClass = object.fromJson(CustomClass.class);
CustomClass compareClass = new CustomClass(12, "Alex", 1500000000L);
assertEquals(customClass, compareClass);
}
public static class CustomClass {
public int number;
public String name;
public Long longNumber;
public CustomClass() {}
public CustomClass (int number, String name, Long longNumber) {
this.number = number;
this.name = name;
this.longNumber = longNumber;
}
@Override
public boolean equals(Object o) {
CustomClass customClass = (CustomClass) o;
return (this.number == customClass.number
&& this.name.equals(customClass.name)
&& this.longNumber.equals(customClass.longNumber));
}
}
@Test
public void jsonObjectParseFromJson_1() {
JSONBuilder builder = new JSONBuilder();
builder.setClassMapping(java.time.LocalDateTime.class, s -> java.time.LocalDateTime.parse((String)s));
JSONObject object = new JSONObject(builder);
java.time.LocalDateTime localDateTime = java.time.LocalDateTime.now();
object.put("localDate", localDateTime.toString());
CustomClassA customClassA = object.fromJson(CustomClassA.class);
CustomClassA compareClassClassA = new CustomClassA(localDateTime);
assertEquals(customClassA, compareClassClassA);
}
public static class CustomClassA {
public java.time.LocalDateTime localDate;
public CustomClassA() {}
public CustomClassA(java.time.LocalDateTime localDate) {
this.localDate = localDate;
}
@Override
public boolean equals(Object o) {
CustomClassA classA = (CustomClassA) o;
return this.localDate.equals(classA.localDate);
}
}
@Test
public void jsonObjectParseFromJson_2() {
JSONObject object = new JSONObject();
object.put("number", 12);
JSONObject classC = new JSONObject();
classC.put("stringName", "Alex");
classC.put("longNumber", 123456L);
object.put("classC", classC);
CustomClassB customClassB = object.fromJson(CustomClassB.class);
CustomClassC classCObject = new CustomClassC("Alex", 123456L);
CustomClassB compareClassB = new CustomClassB(12, classCObject);
assertEquals(customClassB, compareClassB);
}
public static class CustomClassB {
public int number;
public CustomClassC classC;
public CustomClassB() {}
public CustomClassB(int number, CustomClassC classC) {
this.number = number;
this.classC = classC;
}
@Override
public boolean equals(Object o) {
CustomClassB classB = (CustomClassB) o;
return this.number == classB.number
&& this.classC.equals(classB.classC);
}
}
public static class CustomClassC {
public String stringName;
public Long longNumber;
public CustomClassC() {}
public CustomClassC(String stringName, Long longNumber) {
this.stringName = stringName;
this.longNumber = longNumber;
}
public JSONObject toJSON() {
JSONObject object = new JSONObject();
object.put("stringName", this.stringName);
object.put("longNumber", this.longNumber);
return object;
}
@Override
public boolean equals(Object o) {
CustomClassC classC = (CustomClassC) o;
return this.stringName.equals(classC.stringName)
&& this.longNumber.equals(classC.longNumber);
}
@Override
public int hashCode() {
return java.util.Objects.hash(stringName, longNumber);
}
}
@Test
public void jsonObjectParseFromJson_3() {
JSONObject object = new JSONObject();
JSONArray array = new JSONArray();
array.put("test1");
array.put("test2");
array.put("test3");
object.put("stringList", array);
CustomClassD customClassD = object.fromJson(CustomClassD.class);
CustomClassD compareClassD = new CustomClassD(Arrays.asList("test1", "test2", "test3"));
assertEquals(customClassD, compareClassD);
}
public static class CustomClassD {
public List<String> stringList;
public CustomClassD() {}
public CustomClassD(List<String> stringList) {
this.stringList = stringList;
}
@Override
public boolean equals(Object o) {
CustomClassD classD = (CustomClassD) o;
return this.stringList.equals(classD.stringList);
}
}
@Test
public void jsonObjectParseFromJson_4() {
JSONObject object = new JSONObject();
JSONArray array = new JSONArray();
array.put(new CustomClassC("test1", 1L).toJSON());
array.put(new CustomClassC("test2", 2L).toJSON());
object.put("listClassC", array);
CustomClassE customClassE = object.fromJson(CustomClassE.class);
CustomClassE compareClassE = new CustomClassE(java.util.Arrays.asList(
new CustomClassC("test1", 1L),
new CustomClassC("test2", 2L)));
assertEquals(customClassE, compareClassE);
}
public static class CustomClassE {
public List<CustomClassC> listClassC;
public CustomClassE() {}
public CustomClassE(List<CustomClassC> listClassC) {
this.listClassC = listClassC;
}
@Override
public boolean equals(Object o) {
CustomClassE classE = (CustomClassE) o;
return this.listClassC.equals(classE.listClassC);
}
}
@Test
public void jsonObjectParseFromJson_5() {
JSONObject object = new JSONObject();
JSONArray array = new JSONArray();
array.put(Arrays.asList("A", "B", "C"));
array.put(Arrays.asList("D", "E"));
object.put("listOfString", array);
CustomClassF customClassF = object.fromJson(CustomClassF.class);
List<List<String>> listOfString = new ArrayList<>();
listOfString.add(Arrays.asList("A", "B", "C"));
listOfString.add(Arrays.asList("D", "E"));
CustomClassF compareClassF = new CustomClassF(listOfString);
assertEquals(customClassF, compareClassF);
}
public static class CustomClassF {
public List<List<String>> listOfString;
public CustomClassF() {}
public CustomClassF(List<List<String>> listOfString) {
this.listOfString = listOfString;
}
@Override
public boolean equals(Object o) {
CustomClassF classF = (CustomClassF) o;
return this.listOfString.equals(classF.listOfString);
}
}
}