entry : this.entrySet()) {
String name = entry.getKey();
Object valueThis = entry.getValue();
Object valueOther = ((JSONObject)other).get(name);
if(valueThis == valueOther) {
continue;
}
if(valueThis == null) {
return false;
}
if (!checkObjectType(valueThis, valueOther)) {
return false;
}
}
return true;
}
/**
* Convenience function. Compares types of two objects.
* @param valueThis Object whose type is being checked
* @param valueOther Reference object
* @return true if match, else false
*/
private boolean checkObjectType(Object valueThis, Object valueOther) {
if (valueThis instanceof JSONObject) {
return ((JSONObject)valueThis).similar(valueOther);
} else if (valueThis instanceof JSONArray) {
return ((JSONArray)valueThis).similar(valueOther);
} else if (valueThis instanceof Number && valueOther instanceof Number) {
return isNumberSimilar((Number)valueThis, (Number)valueOther);
} else if (valueThis instanceof JSONString && valueOther instanceof JSONString) {
return ((JSONString) valueThis).toJSONString().equals(((JSONString) valueOther).toJSONString());
} else if (!valueThis.equals(valueOther)) {
return false;
}
return true;
}
/**
* Compares two numbers to see if they are similar.
*
* If either of the numbers are Double or Float instances, then they are checked to have
* a finite value. If either value is not finite (NaN or ±infinity), then this
* function will always return false. If both numbers are finite, they are first checked
* to be the same type and implement {@link Comparable}. If they do, then the actual
* {@link Comparable#compareTo(Object)} is called. If they are not the same type, or don't
* implement Comparable, then they are converted to {@link BigDecimal}s. Finally the
* BigDecimal values are compared using {@link BigDecimal#compareTo(BigDecimal)}.
*
* @param l the Left value to compare. Can not be null.
* @param r the right value to compare. Can not be null.
* @return true if the numbers are similar, false otherwise.
*/
static boolean isNumberSimilar(Number l, Number r) {
if (!numberIsFinite(l) || !numberIsFinite(r)) {
// non-finite numbers are never similar
return false;
}
// if the classes are the same and implement Comparable
// then use the built in compare first.
if(l.getClass().equals(r.getClass()) && l instanceof Comparable) {
@SuppressWarnings({ "rawtypes", "unchecked" })
int compareTo = ((Comparable)l).compareTo(r);
return compareTo==0;
}
// BigDecimal should be able to handle all of our number types that we support through
// documentation. Convert to BigDecimal first, then use the Compare method to
// decide equality.
final BigDecimal lBigDecimal = objectToBigDecimal(l, null, false);
final BigDecimal rBigDecimal = objectToBigDecimal(r, null, false);
if (lBigDecimal == null || rBigDecimal == null) {
return false;
}
return lBigDecimal.compareTo(rBigDecimal) == 0;
}
private static boolean numberIsFinite(Number n) {
if (n instanceof Double && (((Double) n).isInfinite() || ((Double) n).isNaN())) {
return false;
} else if (n instanceof Float && (((Float) n).isInfinite() || ((Float) n).isNaN())) {
return false;
}
return true;
}
/**
* Tests if the value should be tried as a decimal. It makes no test if there are actual digits.
*
* @param val value to test
* @return true if the string is "-0" or if it contains '.', 'e', or 'E', false otherwise.
*/
protected static boolean isDecimalNotation(final String val) {
return val.indexOf('.') > -1 || val.indexOf('e') > -1
|| val.indexOf('E') > -1 || "-0".equals(val);
}
/**
* Try to convert a string into a number, boolean, or null. If the string
* can't be converted, return the string.
*
* @param string
* A String. can not be null.
* @return A simple JSON value.
* @throws NullPointerException
* Thrown if the string is null.
*/
// Changes to this method must be copied to the corresponding method in
// the XML class to keep full support for Android
public static Object stringToValue(String string) {
if ("".equals(string)) {
return string;
}
// check JSON key words true/false/null
if ("true".equalsIgnoreCase(string)) {
return Boolean.TRUE;
}
if ("false".equalsIgnoreCase(string)) {
return Boolean.FALSE;
}
if ("null".equalsIgnoreCase(string)) {
return JSONObject.NULL;
}
/*
* If it might be a number, try converting it. If a number cannot be
* produced, then the value will just be a string.
*/
char initial = string.charAt(0);
if ((initial >= '0' && initial <= '9') || initial == '-') {
try {
return stringToNumber(string);
} catch (Exception ignore) {
// Do nothing
}
}
return string;
}
/**
* Converts a string to a number using the narrowest possible type. Possible
* returns for this function are BigDecimal, Double, BigInteger, Long, and Integer.
* When a Double is returned, it should always be a valid Double and not NaN or +-infinity.
*
* @param val value to convert
* @return Number representation of the value.
* @throws NumberFormatException thrown if the value is not a valid number. A public
* caller should catch this and wrap it in a {@link JSONException} if applicable.
*/
protected static Number stringToNumber(final String val) throws NumberFormatException {
char initial = val.charAt(0);
if ((initial >= '0' && initial <= '9') || initial == '-') {
// decimal representation
if (isDecimalNotation(val)) {
return getNumber(val, initial);
}
// block items like 00 01 etc. Java number parsers treat these as Octal.
checkForInvalidNumberFormat(val, initial);
// integer representation.
// This will narrow any values to the smallest reasonable Object representation
// (Integer, Long, or BigInteger)
// BigInteger down conversion: We use a similar bitLength compare as
// BigInteger#intValueExact uses. Increases GC, but objects hold
// only what they need. i.e. Less runtime overhead if the value is
// long lived.
BigInteger bi = new BigInteger(val);
if(bi.bitLength() <= 31){
return Integer.valueOf(bi.intValue());
}
if(bi.bitLength() <= 63){
return Long.valueOf(bi.longValue());
}
return bi;
}
throw new NumberFormatException("val ["+val+"] is not a valid number.");
}
/**
* Convenience function. Block items like 00 01 etc. Java number parsers treat these as Octal.
* @param val value to convert
* @param initial first char of val
* @throws exceptions if numbers are formatted incorrectly
*/
private static void checkForInvalidNumberFormat(String val, char initial) {
if(initial == '0' && val.length() > 1) {
char at1 = val.charAt(1);
if(at1 >= '0' && at1 <= '9') {
throw new NumberFormatException("val ["+ val +"] is not a valid number.");
}
} else if (initial == '-' && val.length() > 2) {
char at1 = val.charAt(1);
char at2 = val.charAt(2);
if(at1 == '0' && at2 >= '0' && at2 <= '9') {
throw new NumberFormatException("val ["+ val +"] is not a valid number.");
}
}
}
/**
* Convenience function. Handles val if it is a number
* @param val value to convert
* @param initial first char of val
* @return val as a BigDecimal
*/
private static Number getNumber(String val, char initial) {
// Use a BigDecimal all the time so we keep the original
// representation. BigDecimal doesn't support -0.0, ensure we
// keep that by forcing a decimal.
try {
BigDecimal bd = new BigDecimal(val);
if(initial == '-' && BigDecimal.ZERO.compareTo(bd)==0) {
return Double.valueOf(-0.0);
}
return bd;
} catch (NumberFormatException retryAsDouble) {
// this is to support "Hex Floats" like this: 0x1.0P-1074
try {
Double d = Double.valueOf(val);
if(d.isNaN() || d.isInfinite()) {
throw new NumberFormatException("val ["+ val +"] is not a valid number.");
}
return d;
} catch (NumberFormatException ignore) {
throw new NumberFormatException("val ["+ val +"] is not a valid number.");
}
}
}
/**
* Throw an exception if the object is a NaN or infinite number.
*
* @param o
* The object to test.
* @throws JSONException
* If o is a non-finite number.
*/
public static void testValidity(Object o) throws JSONException {
if (o instanceof Number && !numberIsFinite((Number) o)) {
throw new JSONException("JSON does not allow non-finite numbers.");
}
}
/**
* Produce a JSONArray containing the values of the members of this
* JSONObject.
*
* @param names
* A JSONArray containing a list of key strings. This determines
* the sequence of the values in the result.
* @return A JSONArray of values.
* @throws JSONException
* If any of the values are non-finite numbers.
*/
public JSONArray toJSONArray(JSONArray names) throws JSONException {
if (names == null || names.isEmpty()) {
return null;
}
JSONArray ja = new JSONArray();
for (int i = 0; i < names.length(); i += 1) {
ja.put(this.opt(names.getString(i)));
}
return ja;
}
/**
* Make a JSON text of this JSONObject. For compactness, no whitespace is
* added. If this would not result in a syntactically correct JSON text,
* then null will be returned instead.
*
* Warning: This method assumes that the data structure is acyclical.
*
*
* @return a printable, displayable, portable, transmittable representation
* of the object, beginning with { (left
* brace) and ending with } (right
* brace) .
*/
@Override
public String toString() {
try {
return this.toString(0);
} catch (Exception e) {
return null;
}
}
/**
* Make a pretty-printed JSON text of this JSONObject.
*
*
If
{@code indentFactor > 0} and the {@link JSONObject}
* has only one key, then the object will be output on a single line:
* {@code {"key": 1}}
*
* If an object has 2 or more keys, then it will be output across
* multiple lines:
{@code {
* "key1": 1,
* "key2": "value 2",
* "key3": 3
* }}
*
* Warning: This method assumes that the data structure is acyclical.
*
*
* @param indentFactor
* The number of spaces to add to each level of indentation.
* @return a printable, displayable, portable, transmittable representation
* of the object, beginning with { (left
* brace) and ending with } (right
* brace) .
* @throws JSONException
* If the object contains an invalid number.
*/
@SuppressWarnings("resource")
public String toString(int indentFactor) throws JSONException {
// 6 characters are the minimum to serialise a key value pair e.g.: "k":1,
// and we don't want to oversize the initial capacity
int initialSize = map.size() * 6;
Writer w = new StringBuilderWriter(Math.max(initialSize, 16));
return this.write(w, indentFactor, 0).toString();
}
/**
* Make a JSON text of an Object value. If the object has an
* value.toJSONString() method, then that method will be used to produce the
* JSON text. The method is required to produce a strictly conforming text.
* If the object does not contain a toJSONString method (which is the most
* common case), then a text will be produced by other means. If the value
* is an array or Collection, then a JSONArray will be made from it and its
* toJSONString method will be called. If the value is a MAP, then a
* JSONObject will be made from it and its toJSONString method will be
* called. Otherwise, the value's toString method will be called, and the
* result will be quoted.
*
*
* Warning: This method assumes that the data structure is acyclical.
*
* @param value
* The value to be serialized.
* @return a printable, displayable, transmittable representation of the
* object, beginning with { (left
* brace) and ending with } (right
* brace) .
* @throws JSONException
* If the value is or contains an invalid number.
*/
public static String valueToString(Object value) throws JSONException {
// moves the implementation to JSONWriter as:
// 1. It makes more sense to be part of the writer class
// 2. For Android support this method is not available. By implementing it in the Writer
// Android users can use the writer with the built in Android JSONObject implementation.
return JSONWriter.valueToString(value);
}
/**
* Wrap an object, if necessary. If the object is null, return the NULL
* object. If it is an array or collection, wrap it in a JSONArray. If it is
* a map, wrap it in a JSONObject. If it is a standard property (Double,
* String, et al) then it is already wrapped. Otherwise, if it comes from
* one of the java packages, turn it into a string. And if it doesn't, try
* to wrap it in a JSONObject. If the wrapping fails, then null is returned.
*
* @param object
* The object to wrap
* @return The wrapped value
*/
public static Object wrap(Object object) {
return wrap(object, null);
}
/**
* Wrap an object, if necessary. If the object is null, return the NULL
* object. If it is an array or collection, wrap it in a JSONArray. If it is
* a map, wrap it in a JSONObject. If it is a standard property (Double,
* String, et al) then it is already wrapped. Otherwise, if it comes from
* one of the java packages, turn it into a string. And if it doesn't, try
* to wrap it in a JSONObject. If the wrapping fails, then null is returned.
*
* @param object
* The object to wrap
* @param recursionDepth
* Variable for tracking the count of nested object creations.
* @param jsonParserConfiguration
* Variable to pass parser custom configuration for json parsing.
* @return The wrapped value
*/
static Object wrap(Object object, int recursionDepth, JSONParserConfiguration jsonParserConfiguration) {
return wrap(object, null, recursionDepth, jsonParserConfiguration);
}
private static Object wrap(Object object, Set objectsRecord) {
return wrap(object, objectsRecord, 0, new JSONParserConfiguration());
}
private static Object wrap(Object object, Set objectsRecord, int recursionDepth, JSONParserConfiguration jsonParserConfiguration) {
try {
if (NULL.equals(object)) {
return NULL;
}
if (object instanceof JSONObject || object instanceof JSONArray
|| object instanceof JSONString || object instanceof String
|| object instanceof Byte || object instanceof Character
|| object instanceof Short || object instanceof Integer
|| object instanceof Long || object instanceof Boolean
|| object instanceof Float || object instanceof Double
|| object instanceof BigInteger || object instanceof BigDecimal
|| object instanceof Enum) {
return object;
}
if (object instanceof Collection) {
Collection> coll = (Collection>) object;
return new JSONArray(coll, recursionDepth, jsonParserConfiguration);
}
if (object.getClass().isArray()) {
return new JSONArray(object);
}
if (object instanceof Map) {
Map, ?> map = (Map, ?>) object;
return new JSONObject(map, recursionDepth, jsonParserConfiguration);
}
Package objectPackage = object.getClass().getPackage();
String objectPackageName = objectPackage != null ? objectPackage
.getName() : "";
if (objectPackageName.startsWith("java.")
|| objectPackageName.startsWith("javax.")
|| object.getClass().getClassLoader() == null) {
return object.toString();
}
if (objectsRecord != null) {
return new JSONObject(object, objectsRecord);
}
return new JSONObject(object);
}
catch (JSONException exception) {
throw exception;
} catch (Exception exception) {
return null;
}
}
/**
* Write the contents of the JSONObject as JSON text to a writer. For
* compactness, no whitespace is added.
*
* Warning: This method assumes that the data structure is acyclical.
*
* @param writer the writer object
* @return The writer.
* @throws JSONException if a called function has an error
*/
public Writer write(Writer writer) throws JSONException {
return this.write(writer, 0, 0);
}
@SuppressWarnings("resource")
static final Writer writeValue(Writer writer, Object value,
int indentFactor, int indent) throws JSONException, IOException {
if (value == null || value.equals(null)) {
writer.write("null");
} else if (value instanceof JSONString) {
// may throw an exception
processJsonStringToWriteValue(writer, value);
} else if (value instanceof String) {
// assuming most values are Strings, so testing it early
quote(value.toString(), writer);
return writer;
} else if (value instanceof Number) {
// may throw an exception
processNumberToWriteValue(writer, (Number) value);
} else if (value instanceof Boolean) {
writer.write(value.toString());
} else if (value instanceof Enum>) {
writer.write(quote(((Enum>)value).name()));
} else if (value instanceof JSONObject) {
((JSONObject) value).write(writer, indentFactor, indent);
} else if (value instanceof JSONArray) {
((JSONArray) value).write(writer, indentFactor, indent);
} else if (value instanceof Map) {
Map, ?> map = (Map, ?>) value;
new JSONObject(map).write(writer, indentFactor, indent);
} else if (value instanceof Collection) {
Collection> coll = (Collection>) value;
new JSONArray(coll).write(writer, indentFactor, indent);
} else if (value.getClass().isArray()) {
new JSONArray(value).write(writer, indentFactor, indent);
} else {
quote(value.toString(), writer);
}
return writer;
}
/**
* Convenience function to reduce cog complexity of calling method; writes value if string is valid
* @param writer Object doing the writing
* @param value Value to be written
* @throws IOException if something goes wrong
*/
private static void processJsonStringToWriteValue(Writer writer, Object value) throws IOException {
// JSONString must be checked first, so it can overwrite behaviour of other types below
Object o;
try {
o = ((JSONString) value).toJSONString();
} catch (Exception e) {
throw new JSONException(e);
}
writer.write(o != null ? o.toString() : quote(value.toString()));
}
/**
* Convenience function to reduce cog complexity of calling method; writes value if number is valid
* @param writer Object doing the writing
* @param value Value to be written
* @throws IOException if something goes wrong
*/
private static void processNumberToWriteValue(Writer writer, Number value) throws IOException {
// not all Numbers may match actual JSON Numbers. i.e. fractions or Imaginary
final String numberAsString = numberToString(value);
if(NUMBER_PATTERN.matcher(numberAsString).matches()) {
writer.write(numberAsString);
} else {
// The Number value is not a valid JSON number.
// Instead we will quote it as a string
quote(numberAsString, writer);
}
}
static final void indent(Writer writer, int indent) throws IOException {
for (int i = 0; i < indent; i += 1) {
writer.write(' ');
}
}
/**
* Write the contents of the JSONObject as JSON text to a writer.
*
*
If
{@code indentFactor > 0} and the {@link JSONObject}
* has only one key, then the object will be output on a single line:
* {@code {"key": 1}}
*
* If an object has 2 or more keys, then it will be output across
* multiple lines:
{@code {
* "key1": 1,
* "key2": "value 2",
* "key3": 3
* }}
*
* Warning: This method assumes that the data structure is acyclical.
*
*
* @param writer
* Writes the serialized JSON
* @param indentFactor
* The number of spaces to add to each level of indentation.
* @param indent
* The indentation of the top level.
* @return The writer.
* @throws JSONException if a called function has an error or a write error
* occurs
*/
@SuppressWarnings("resource")
public Writer write(Writer writer, int indentFactor, int indent)
throws JSONException {
try {
boolean needsComma = false;
final int length = this.length();
writer.write('{');
if (length == 1) {
final Entry entry = this.entrySet().iterator().next();
final String key = entry.getKey();
writer.write(quote(key));
writer.write(':');
if (indentFactor > 0) {
writer.write(' ');
}
// might throw an exception
attemptWriteValue(writer, indentFactor, indent, entry, key);
} else if (length != 0) {
writeContent(writer, indentFactor, indent, needsComma);
}
writer.write('}');
return writer;
} catch (IOException exception) {
throw new JSONException(exception);
}
}
/**
* Convenience function. Writer attempts to write formatted content
* @param writer
* Writes the serialized JSON
* @param indentFactor
* The number of spaces to add to each level of indentation.
* @param indent
* The indentation of the top level.
* @param needsComma
* Boolean flag indicating a comma is needed
* @throws IOException
* If something goes wrong
*/
private void writeContent(Writer writer, int indentFactor, int indent, boolean needsComma) throws IOException {
final int newIndent = indent + indentFactor;
for (final Entry entry : this.entrySet()) {
if (needsComma) {
writer.write(',');
}
if (indentFactor > 0) {
writer.write('\n');
}
indent(writer, newIndent);
final String key = entry.getKey();
writer.write(quote(key));
writer.write(':');
if (indentFactor > 0) {
writer.write(' ');
}
attemptWriteValue(writer, indentFactor, newIndent, entry, key);
needsComma = true;
}
if (indentFactor > 0) {
writer.write('\n');
}
indent(writer, indent);
}
/**
* Convenience function. Writer attempts to write a value.
* @param writer
* Writes the serialized JSON
* @param indentFactor
* The number of spaces to add to each level of indentation.
* @param indent
* The indentation of the top level.
* @param entry
* Contains the value being written
* @param key
* Identifies the value
* @throws JSONException if a called function has an error or a write error
* occurs
*/
private static void attemptWriteValue(Writer writer, int indentFactor, int indent, Entry entry, String key) {
try{
writeValue(writer, entry.getValue(), indentFactor, indent);
} catch (Exception e) {
throw new JSONException("Unable to write JSONObject value for key: " + key, e);
}
}
/**
* Returns a java.util.Map containing all of the entries in this object.
* If an entry in the object is a JSONArray or JSONObject it will also
* be converted.
*
* Warning: This method assumes that the data structure is acyclical.
*
* @return a java.util.Map containing the entries of this object
*/
public Map toMap() {
Map results = new HashMap();
for (Entry entry : this.entrySet()) {
Object value;
if (entry.getValue() == null || NULL.equals(entry.getValue())) {
value = null;
} else if (entry.getValue() instanceof JSONObject) {
value = ((JSONObject) entry.getValue()).toMap();
} else if (entry.getValue() instanceof JSONArray) {
value = ((JSONArray) entry.getValue()).toList();
} else {
value = entry.getValue();
}
results.put(entry.getKey(), value);
}
return results;
}
/**
* Create a new JSONException in a common format for incorrect conversions.
* @param key name of the key
* @param valueType the type of value being coerced to
* @param cause optional cause of the coercion failure
* @return JSONException that can be thrown.
*/
private static JSONException wrongValueFormatException(
String key,
String valueType,
Object value,
Throwable cause) {
if(value == null) {
return new JSONException(
"JSONObject[" + quote(key) + "] is not a " + valueType + " (null)."
, cause);
}
// don't try to toString collections or known object types that could be large.
if(value instanceof Map || value instanceof Iterable || value instanceof JSONObject) {
return new JSONException(
"JSONObject[" + quote(key) + "] is not a " + valueType + " (" + value.getClass() + ")."
, cause);
}
return new JSONException(
"JSONObject[" + quote(key) + "] is not a " + valueType + " (" + value.getClass() + " : " + value + ")."
, cause);
}
/**
* Create a new JSONException in a common format for recursive object definition.
* @param key name of the key
* @return JSONException that can be thrown.
*/
private static JSONException recursivelyDefinedObjectException(String key) {
return new JSONException(
"JavaBean object contains recursively defined member variable of key " + quote(key)
);
}
/**
* Helper method to extract the raw Class from Type.
*/
private Class> getRawType(Type type) {
if (type instanceof Class) {
return (Class>) type;
} else if (type instanceof ParameterizedType) {
return (Class>) ((ParameterizedType) type).getRawType();
} else if (type instanceof GenericArrayType) {
return Object[].class; // Simplified handling for arrays
}
return Object.class; // Fallback
}
/**
* Extracts the element Type for a Collection Type.
*/
private Type getElementType(Type type) {
if (type instanceof ParameterizedType) {
Type[] args = ((ParameterizedType) type).getActualTypeArguments();
return args.length > 0 ? args[0] : Object.class;
}
return Object.class;
}
/**
* Extracts the key and value Types for a Map Type.
*/
private Type[] getMapTypes(Type type) {
if (type instanceof ParameterizedType) {
Type[] args = ((ParameterizedType) type).getActualTypeArguments();
if (args.length == 2) {
return args;
}
}
return new Type[]{Object.class, Object.class}; // Default: String keys, Object values
}
/**
* Deserializes a JSON string into an instance of the specified class.
*
* 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 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
*/
public static T fromJson(String jsonString, Class clazz) {
JSONObject jsonObject = new JSONObject(jsonString);
return jsonObject.fromJson(clazz);
}
/**
* Deserializes a JSON string into an instance of the specified class.
*
* This method attempts to map JSON key-value pairs to the corresponding fields
* of the given class. It supports basic data types including {@code int}, {@code double},
* {@code float}, {@code long}, and {@code boolean}, as well as their boxed counterparts.
* The target class must have a no-argument constructor, and its field names must match
* the keys in the JSON string.
*
*
Note: 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.
*
* @param clazz the class of the object to be returned
* @param 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
*/
@SuppressWarnings("unchecked")
public T fromJson(Class clazz) {
try {
T obj = clazz.getDeclaredConstructor().newInstance();
for (Field field : clazz.getDeclaredFields()) {
field.setAccessible(true);
String fieldName = field.getName();
if (has(fieldName)) {
Object value = get(fieldName);
Type fieldType = field.getGenericType();
Class> rawType = getRawType(fieldType);
if (classMapping.containsKey(rawType)) {
field.set(obj, classMapping.get(rawType).convert(value));
} else {
Object convertedValue = convertValue(value, fieldType);
field.set(obj, convertedValue);
}
}
}
return obj;
} catch (NoSuchMethodException e) {
throw new JSONException("No no-arg constructor for class: " + clazz.getName(), e);
} catch (Exception e) {
throw new JSONException("Failed to instantiate or set field for class: " + clazz.getName(), e);
}
}
/**
* Recursively converts a value to the target Type, handling nested generics for Collections and Maps.
*/
private Object convertValue(Object value, Type targetType) throws JSONException {
if (value == null) {
return null;
}
Class> rawType = getRawType(targetType);
// Direct assignment
if (rawType.isAssignableFrom(value.getClass())) {
return value;
}
// Use registered type converter
if (classMapping.containsKey(rawType)) {
return classMapping.get(rawType).convert(value);
}
// Enum conversion
if (rawType.isEnum() && value instanceof String) {
return stringToEnum(rawType, (String) value);
}
// Collection handling (e.g., List>>)
if (Collection.class.isAssignableFrom(rawType)) {
if (value instanceof JSONArray) {
Type elementType = getElementType(targetType);
return fromJsonArray((JSONArray) value, rawType, elementType);
}
}
// Map handling (e.g., Map>)
else if (Map.class.isAssignableFrom(rawType) && value instanceof JSONObject) {
Type[] mapTypes = getMapTypes(targetType);
Type keyType = mapTypes[0];
Type valueType = mapTypes[1];
return convertToMap((JSONObject) value, keyType, valueType, rawType);
}
// POJO handling (including custom classes like Tuple)
else if (!rawType.isPrimitive() && !rawType.isEnum() && value instanceof JSONObject) {
// Recurse with the raw class for POJO deserialization
return ((JSONObject) value).fromJson(rawType);
}
// Fallback
return value.toString();
}
/**
* Converts a JSONObject to a Map with the specified generic key and value Types.
* Supports nested types via recursive convertValue.
*/
private Map, ?> convertToMap(JSONObject jsonMap, Type keyType, Type valueType, Class> mapType) throws JSONException {
try {
InstanceCreator> creator = collectionMapping.get(mapType) != null ? collectionMapping.get(mapType) : new InstanceCreator() {
public Map create() {
return new HashMap();
}
};
@SuppressWarnings("unchecked")
Map createdMap = (Map) creator.create();
for (Object keyObj : jsonMap.keySet()) {
String keyStr = (String) keyObj;
Object mapValue = jsonMap.get(keyStr);
// Convert key (e.g., String to Integer for Map)
Object convertedKey = convertValue(keyStr, keyType);
// Convert value recursively (handles nesting)
Object convertedValue = convertValue(mapValue, valueType);
createdMap.put(convertedKey, convertedValue);
}
return createdMap;
} catch (Exception e) {
throw new JSONException("Failed to convert JSONObject to Map: " + mapType.getName(), e);
}
}
/**
* Converts a String to an Enum value.
*/
private E stringToEnum(Class> enumClass, String value) throws JSONException {
try {
@SuppressWarnings("unchecked")
Class enumType = (Class) enumClass;
Method valueOfMethod = enumType.getMethod("valueOf", String.class);
return (E) valueOfMethod.invoke(null, value);
} catch (Exception e) {
throw new JSONException("Failed to convert string to enum: " + value + " for " + enumClass.getName(), e);
}
}
/**
* Deserializes a JSONArray into a Collection, supporting nested generics.
* Uses recursive convertValue for elements.
*/
@SuppressWarnings("unchecked")
private Collection fromJsonArray(JSONArray jsonArray, Class> collectionType, Type elementType) throws JSONException {
try {
InstanceCreator> creator = collectionMapping.get(collectionType) != null ? collectionMapping.get(collectionType) : new InstanceCreator() {
public List create() {
return new ArrayList();
}
};
Collection collection = (Collection) creator.create();
for (int i = 0; i < jsonArray.length(); i++) {
Object jsonElement = jsonArray.get(i);
// Recursively convert each element using the full element Type (handles nesting)
Object convertedValue = convertValue(jsonElement, elementType);
collection.add((T) convertedValue);
}
return collection;
} catch (Exception e) {
throw new JSONException("Failed to convert JSONArray to Collection: " + collectionType.getName(), e);
}
}
}