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
@@ -2996,22 +3287,248 @@ private static JSONException recursivelyDefinedObjectException(String key) {
}
/**
- * For a prospective number, remove the leading zeros
- * @param value prospective number
- * @return number without leading zeros
+ * 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();
+ 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;
+ }
+
+ if (rawType == int.class || rawType == Integer.class) {
+ return ((Number) value).intValue();
+ } else if (rawType == double.class || rawType == Double.class) {
+ return ((Number) value).doubleValue();
+ } else if (rawType == float.class || rawType == Float.class) {
+ return ((Number) value).floatValue();
+ } else if (rawType == long.class || rawType == Long.class) {
+ return ((Number) value).longValue();
+ } else if (rawType == boolean.class || rawType == Boolean.class) {
+ return value;
+ } else if (rawType == String.class) {
+ return value;
+ } else if (rawType == BigDecimal.class) {
+ return new BigDecimal((String) value);
+ } else if (rawType == BigInteger.class) {
+ return new BigInteger((String) 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 static String removeLeadingZerosOfNumber(String value){
- if (value.equals("-")){return value;}
- boolean negativeFirstChar = (value.charAt(0) == '-');
- int counter = negativeFirstChar ? 1:0;
- while (counter < value.length()){
- if (value.charAt(counter) != '0'){
- if (negativeFirstChar) {return "-".concat(value.substring(counter));}
- return value.substring(counter);
+ private Map, ?> convertToMap(JSONObject jsonMap, Type keyType, Type valueType, Class> mapType) throws JSONException {
+ try {
+ @SuppressWarnings("unchecked")
+ Map createdMap = new HashMap();
+
+ 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);
}
- ++counter;
+ 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 {
+ Collection collection = getCollection(collectionType);
+
+ 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);
+ }
+ }
+
+ /**
+ * Creates and returns a new instance of a supported {@link Collection} implementation
+ * based on the specified collection type.
+ *
+ * This method currently supports the following collection types:
+ *
+ * {@code List.class}
+ * {@code ArrayList.class}
+ * {@code Set.class}
+ * {@code HashSet.class}
+ *
+ * If the provided type does not match any of the supported types, a {@link JSONException}
+ * is thrown.
+ *
+ * @param collectionType the {@link Class} object representing the desired collection type
+ * @return a new empty instance of the specified collection type
+ * @throws JSONException if the specified type is not a supported collection type
+ */
+ private Collection getCollection(Class> collectionType) throws JSONException {
+ if (collectionType == List.class || collectionType == ArrayList.class) {
+ return new ArrayList();
+ } else if (collectionType == Set.class || collectionType == HashSet.class) {
+ return new HashSet();
+ } else {
+ throw new JSONException("Unsupported Collection type: " + collectionType.getName());
}
- if (negativeFirstChar) {return "-0";}
- return "0";
}
}
diff --git a/src/main/java/org/json/JSONParserConfiguration.java b/src/main/java/org/json/JSONParserConfiguration.java
index ad0d7fb72..0cfa2eaef 100644
--- a/src/main/java/org/json/JSONParserConfiguration.java
+++ b/src/main/java/org/json/JSONParserConfiguration.java
@@ -4,24 +4,15 @@
* Configuration object for the JSON parser. The configuration is immutable.
*/
public class JSONParserConfiguration extends ParserConfiguration {
-
- /** Original Configuration of the JSON Parser. */
- public static final JSONParserConfiguration ORIGINAL = new JSONParserConfiguration();
-
- /** Original configuration of the JSON Parser except that values are kept as strings. */
- public static final JSONParserConfiguration KEEP_STRINGS = new JSONParserConfiguration().withKeepStrings(true);
-
/**
* Used to indicate whether to overwrite duplicate key or not.
*/
private boolean overwriteDuplicateKey;
-
+
/**
- * This flag, when set to true, instructs the parser to throw a JSONException if it encounters an invalid character
- * immediately following the final ']' character in the input. This is useful for ensuring strict adherence to the
- * JSON syntax, as any characters after the final closing bracket of a JSON array are considered invalid.
+ * Used to indicate whether to convert java null values to JSONObject.NULL or ignoring the entry when converting java maps.
*/
- private boolean strictMode;
+ private boolean useNativeNulls;
/**
* Configuration with the default values.
@@ -29,13 +20,24 @@ public class JSONParserConfiguration extends ParserConfiguration {
public JSONParserConfiguration() {
super();
this.overwriteDuplicateKey = false;
+ // DO NOT DELETE THE FOLLOWING LINE -- it is used for strictMode testing
+ // this.strictMode = true;
}
+ /**
+ * This flag, when set to true, instructs the parser to enforce strict mode when parsing JSON text.
+ * Garbage chars at the end of the doc, unquoted string, and single-quoted strings are all disallowed.
+ */
+ private boolean strictMode;
+
@Override
protected JSONParserConfiguration clone() {
JSONParserConfiguration clone = new JSONParserConfiguration();
clone.overwriteDuplicateKey = overwriteDuplicateKey;
+ clone.strictMode = strictMode;
clone.maxNestingDepth = maxNestingDepth;
+ clone.keepStrings = keepStrings;
+ clone.useNativeNulls = useNativeNulls;
return clone;
}
@@ -71,7 +73,33 @@ public JSONParserConfiguration withOverwriteDuplicateKey(final boolean overwrite
return clone;
}
+
+ /**
+ * Controls the parser's behavior when meeting Java null values while converting maps.
+ * If set to true, the parser will put a JSONObject.NULL into the resulting JSONObject.
+ * Or the map entry will be ignored.
+ *
+ * @param useNativeNulls defines if the parser should convert null values in Java maps
+ * @return The existing configuration will not be modified. A new configuration is returned.
+ */
+ public JSONParserConfiguration withUseNativeNulls(final boolean useNativeNulls) {
+ JSONParserConfiguration clone = this.clone();
+ clone.useNativeNulls = useNativeNulls;
+ return clone;
+ }
+
+ /**
+ * Sets the strict mode configuration for the JSON parser with default true value
+ *
+ * When strict mode is enabled, the parser will throw a JSONException if it encounters an invalid character
+ * immediately following the final ']' character in the input. This is useful for ensuring strict adherence to the
+ * JSON syntax, as any characters after the final closing bracket of a JSON array are considered invalid.
+ * @return a new JSONParserConfiguration instance with the updated strict mode setting
+ */
+ public JSONParserConfiguration withStrictMode() {
+ return withStrictMode(true);
+ }
/**
* Sets the strict mode configuration for the JSON parser.
@@ -99,16 +127,24 @@ public JSONParserConfiguration withStrictMode(final boolean mode) {
public boolean isOverwriteDuplicateKey() {
return this.overwriteDuplicateKey;
}
-
+
+ /**
+ * The parser's behavior when meeting a null value in a java map, controls whether the parser should
+ * write a JSON entry with a null value (isUseNativeNulls() == true)
+ * or ignore that map entry (isUseNativeNulls() == false).
+ *
+ * @return The useNativeNulls configuration value.
+ */
+ public boolean isUseNativeNulls() {
+ return this.useNativeNulls;
+ }
+
/**
- * Retrieves the current strict mode setting of the JSON parser.
- *
- * Strict mode, when enabled, instructs the parser to throw a JSONException if it encounters an invalid character
- * immediately following the final ']' character in the input. This ensures strict adherence to the JSON syntax, as
- * any characters after the final closing bracket of a JSON array are considered invalid.
+ * The parser throws an Exception when strict mode is true and tries to parse invalid JSON characters.
+ * Otherwise, the parser is more relaxed and might tolerate some invalid characters.
*
- * @return the current strict mode setting. True if strict mode is enabled, false otherwise.
+ * @return the current strict mode setting.
*/
public boolean isStrictMode() {
return this.strictMode;
diff --git a/src/main/java/org/json/JSONPointer.java b/src/main/java/org/json/JSONPointer.java
index 859e1e644..34066c1aa 100644
--- a/src/main/java/org/json/JSONPointer.java
+++ b/src/main/java/org/json/JSONPointer.java
@@ -127,7 +127,7 @@ public JSONPointer(final String pointer) {
if (pointer == null) {
throw new NullPointerException("pointer cannot be null");
}
- if (pointer.isEmpty() || pointer.equals("#")) {
+ if (pointer.isEmpty() || "#".equals(pointer)) {
this.refTokens = Collections.emptyList();
return;
}
@@ -246,7 +246,7 @@ private static Object readByIndexToken(Object current, String indexToken) throws
*/
@Override
public String toString() {
- StringBuilder rval = new StringBuilder("");
+ StringBuilder rval = new StringBuilder();
for (String token: this.refTokens) {
rval.append('/').append(escape(token));
}
diff --git a/src/main/java/org/json/JSONTokener.java b/src/main/java/org/json/JSONTokener.java
index 63effc5f7..07ff18c99 100644
--- a/src/main/java/org/json/JSONTokener.java
+++ b/src/main/java/org/json/JSONTokener.java
@@ -2,8 +2,6 @@
import java.io.*;
import java.nio.charset.Charset;
-import java.util.ArrayList;
-import java.util.List;
/*
Public Domain.
@@ -33,16 +31,28 @@ public class JSONTokener {
private boolean usePrevious;
/** the number of characters read in the previous line. */
private long characterPreviousLine;
- private final List smallCharMemory;
- private int arrayLevel = 0;
+ // access to this object is required for strict mode checking
+ private JSONParserConfiguration jsonParserConfiguration;
/**
* Construct a JSONTokener from a Reader. The caller must close the Reader.
*
- * @param reader A reader.
+ * @param reader the source.
*/
public JSONTokener(Reader reader) {
+ this(reader, new JSONParserConfiguration());
+ }
+
+ /**
+ * Construct a JSONTokener from a Reader with a given JSONParserConfiguration. The caller must close the Reader.
+ *
+ * @param reader the source.
+ * @param jsonParserConfiguration A JSONParserConfiguration instance that controls the behavior of the parser.
+ *
+ */
+ public JSONTokener(Reader reader, JSONParserConfiguration jsonParserConfiguration) {
+ this.jsonParserConfiguration = jsonParserConfiguration;
this.reader = reader.markSupported()
? reader
: new BufferedReader(reader);
@@ -53,28 +63,62 @@ public JSONTokener(Reader reader) {
this.character = 1;
this.characterPreviousLine = 0;
this.line = 1;
- this.smallCharMemory = new ArrayList(2);
}
-
/**
* Construct a JSONTokener from an InputStream. The caller must close the input stream.
* @param inputStream The source.
*/
public JSONTokener(InputStream inputStream) {
- this(new InputStreamReader(inputStream, Charset.forName("UTF-8")));
+ this(inputStream, new JSONParserConfiguration());
+ }
+
+ /**
+ * Construct a JSONTokener from an InputStream. The caller must close the input stream.
+ * @param inputStream The source.
+ * @param jsonParserConfiguration A JSONParserConfiguration instance that controls the behavior of the parser.
+ */
+ public JSONTokener(InputStream inputStream, JSONParserConfiguration jsonParserConfiguration) {
+ this(new InputStreamReader(inputStream, Charset.forName("UTF-8")), jsonParserConfiguration);
}
/**
* Construct a JSONTokener from a string.
*
- * @param s A source string.
+ * @param source A source string.
+ */
+ public JSONTokener(String source) {
+ this(new StringReader(source));
+ }
+
+ /**
+ * Construct a JSONTokener from an InputStream. The caller must close the input stream.
+ * @param source The source.
+ * @param jsonParserConfiguration A JSONParserConfiguration instance that controls the behavior of the parser.
+ */
+ public JSONTokener(String source, JSONParserConfiguration jsonParserConfiguration) {
+ this(new StringReader(source), jsonParserConfiguration);
+ }
+
+ /**
+ * Getter
+ * @return jsonParserConfiguration
*/
- public JSONTokener(String s) {
- this(new StringReader(s));
+ public JSONParserConfiguration getJsonParserConfiguration() {
+ return jsonParserConfiguration;
}
+ /**
+ * Setter
+ * @param jsonParserConfiguration new value for jsonParserConfiguration
+ *
+ * @deprecated method should not be used
+ */
+ @Deprecated
+ public void setJsonParserConfiguration(JSONParserConfiguration jsonParserConfiguration) {
+ this.jsonParserConfiguration = jsonParserConfiguration;
+ }
/**
* Back up one character. This provides a sort of lookahead capability,
@@ -191,46 +235,6 @@ public char next() throws JSONException {
return this.previous;
}
- private void insertCharacterInCharMemory(Character c) {
- boolean foundSameCharRef = checkForEqualCharRefInMicroCharMemory(c);
- if(foundSameCharRef){
- return;
- }
-
- if(smallCharMemory.size() < 2){
- smallCharMemory.add(c);
- return;
- }
-
- smallCharMemory.set(0, smallCharMemory.get(1));
- smallCharMemory.remove(1);
- smallCharMemory.add(c);
- }
-
- private boolean checkForEqualCharRefInMicroCharMemory(Character c) {
- boolean isNotEmpty = !smallCharMemory.isEmpty();
- if (isNotEmpty) {
- Character lastChar = smallCharMemory.get(smallCharMemory.size() - 1);
- return c.compareTo(lastChar) == 0;
- }
-
- // list is empty so there's no equal characters
- return false;
- }
-
- /**
- * Retrieves the previous char from memory.
- *
- * @return previous char stored in memory.
- */
- public char getPreviousChar() {
- return smallCharMemory.get(0);
- }
-
- public int getArrayLevel(){
- return this.arrayLevel;
- }
-
/**
* Get the last character read from the input or '\0' if nothing has been read yet.
* @return the last character read from the input.
@@ -308,6 +312,7 @@ public String next(int n) throws JSONException {
return new String(chars);
}
+
/**
* Get the next char in the string, skipping whitespace.
* @throws JSONException Thrown if there is an error reading the source string.
@@ -317,7 +322,6 @@ public char nextClean() throws JSONException {
for (;;) {
char c = this.next();
if (c == 0 || c > ' ') {
- insertCharacterInCharMemory(c);
return c;
}
}
@@ -325,14 +329,15 @@ public char nextClean() throws JSONException {
/**
- * Return the characters up to the next close quote character. Backslash processing is done. The formal JSON format
- * does not allow strings in single quotes, but an implementation is allowed to accept them.
- *
+ * Return the characters up to the next close quote character.
+ * Backslash processing is done. The formal JSON format does not
+ * allow strings in single quotes, but an implementation is allowed to
+ * accept them.
* @param quote The quoting character, either
* " (double quote) or
* ' (single quote) .
- * @return A String.
- * @throws JSONException Unterminated string or unbalanced quotes if strictMode == true.
+ * @return A String.
+ * @throws JSONException Unterminated string.
*/
public String nextString(char quote) throws JSONException {
char c;
@@ -340,59 +345,58 @@ public String nextString(char quote) throws JSONException {
for (;;) {
c = this.next();
switch (c) {
- case 0:
- case '\n':
- case '\r':
- throw this.syntaxError("Unterminated string. " +
+ case 0:
+ case '\n':
+ case '\r':
+ throw this.syntaxError("Unterminated string. " +
"Character with int code " + (int) c + " is not allowed within a quoted string.");
- case '\\':
- c = this.next();
- switch (c) {
- case 'b':
- sb.append('\b');
- break;
- case 't':
- sb.append('\t');
- break;
- case 'n':
- sb.append('\n');
- break;
- case 'f':
- sb.append('\f');
- break;
- case 'r':
- sb.append('\r');
- break;
- case 'u':
- String next = this.next(4);
- try {
- sb.append((char) Integer.parseInt(next, 16));
- } catch (NumberFormatException e) {
- throw this.syntaxError("Illegal escape. " +
- "\\u must be followed by a 4 digit hexadecimal number. \\" + next
- + " is not valid.",
- e);
- }
- break;
- case '"':
- case '\'':
- case '\\':
- case '/':
- sb.append(c);
- break;
- default:
- throw this.syntaxError("Illegal escape. Escape sequence \\" + c + " is not valid.");
- }
+ case '\\':
+ c = this.next();
+ switch (c) {
+ case 'b':
+ sb.append('\b');
break;
- default:
- if (c == quote) {
- return sb.toString();
+ case 't':
+ sb.append('\t');
+ break;
+ case 'n':
+ sb.append('\n');
+ break;
+ case 'f':
+ sb.append('\f');
+ break;
+ case 'r':
+ sb.append('\r');
+ break;
+ case 'u':
+ String next = this.next(4);
+ try {
+ sb.append((char)Integer.parseInt(next, 16));
+ } catch (NumberFormatException e) {
+ throw this.syntaxError("Illegal escape. " +
+ "\\u must be followed by a 4 digit hexadecimal number. \\" + next + " is not valid.", e);
}
+ break;
+ case '"':
+ case '\'':
+ case '\\':
+ case '/':
sb.append(c);
+ break;
+ default:
+ throw this.syntaxError("Illegal escape. Escape sequence \\" + c + " is not valid.");
+ }
+ break;
+ default:
+ if (c == quote) {
+ return sb.toString();
+ }
+ sb.append(c);
}
}
}
+
/**
* Get the text up but not including the specified character or the
* end of line, whichever comes first.
@@ -442,113 +446,57 @@ public String nextTo(String delimiters) throws JSONException {
/**
- * Get the next value. The value can be a Boolean, Double, Integer, JSONArray, JSONObject, Long, or String, or the
- * JSONObject.NULL object.
- *
- * @return An object.
+ * Get the next value. The value can be a Boolean, Double, Integer,
+ * JSONArray, JSONObject, Long, or String, or the JSONObject.NULL object.
* @throws JSONException If syntax error.
- */
- public Object nextValue() throws JSONException {
- return nextValue(new JSONParserConfiguration());
- }
-
- /**
- * Get the next value. The value can be a Boolean, Double, Integer, JSONArray, JSONObject, Long, or String, or the
- * JSONObject.NULL object. The strictMode parameter controls the behavior of the method when parsing the value.
*
- * @param jsonParserConfiguration which carries options such as strictMode, these methods will
- * strictly adhere to the JSON syntax, throwing a JSONException for any deviations.
* @return An object.
- * @throws JSONException If syntax error.
*/
- public Object nextValue(JSONParserConfiguration jsonParserConfiguration) throws JSONException {
+ public Object nextValue() throws JSONException {
char c = this.nextClean();
switch (c) {
- case '{':
- this.back();
- try {
- return new JSONObject(this, jsonParserConfiguration);
- } catch (StackOverflowError e) {
- throw new JSONException("JSON Array or Object depth too large to process.", e);
- }
- case '[':
- this.back();
- try {
- this.arrayLevel++;
- return new JSONArray(this, jsonParserConfiguration);
- } catch (StackOverflowError e) {
- throw new JSONException("JSON Array or Object depth too large to process.", e);
- }
- default:
- return nextSimpleValue(c, jsonParserConfiguration);
- }
- }
-
- /**
- * This method is used to get a JSONObject from the JSONTokener. The strictMode parameter controls the behavior of
- * the method when parsing the JSONObject.
- *
- * @param jsonParserConfiguration which carries options such as strictMode, these methods will
- * strictly adhere to the JSON syntax, throwing a JSONException for any deviations.
- * deviations.
- * @return A JSONObject which is the next value in the JSONTokener.
- * @throws JSONException If the JSONObject or JSONArray depth is too large to process.
- */
- private JSONObject getJsonObject(JSONParserConfiguration jsonParserConfiguration) {
- try {
- return new JSONObject(this, jsonParserConfiguration);
- } catch (StackOverflowError e) {
- throw new JSONException("JSON Array or Object depth too large to process.", e);
- }
- }
-
- /**
- * This method is used to get a JSONArray from the JSONTokener.
- *
- * @return A JSONArray which is the next value in the JSONTokener.
- * @throws JSONException If the JSONArray depth is too large to process.
- */
- private JSONArray getJsonArray() {
- try {
- return new JSONArray(this);
- } catch (StackOverflowError e) {
- throw new JSONException("JSON Array or Object depth too large to process.", e);
+ case '{':
+ this.back();
+ try {
+ return new JSONObject(this, jsonParserConfiguration);
+ } catch (StackOverflowError e) {
+ throw new JSONException("JSON Array or Object depth too large to process.", e);
+ }
+ case '[':
+ this.back();
+ try {
+ return new JSONArray(this, jsonParserConfiguration);
+ } catch (StackOverflowError e) {
+ throw new JSONException("JSON Array or Object depth too large to process.", e);
+ }
}
+ return nextSimpleValue(c);
}
- /**
- * Get the next simple value from the JSON input. Simple values include strings (wrapped in single or double
- * quotes), numbers, booleans, and null. This method is called when the next character is not '{' or '['.
- *
- * @param c The starting character.
- * @param jsonParserConfiguration The configuration object containing parsing options.
- * @return The parsed simple value.
- * @throws JSONException If there is a syntax error or the value does not adhere to the configuration rules.
- */
- Object nextSimpleValue(char c, JSONParserConfiguration jsonParserConfiguration) {
- boolean strictMode = jsonParserConfiguration.isStrictMode();
+ Object nextSimpleValue(char c) {
+ String string;
- if (strictMode && c == '\'') {
- throw this.syntaxError("Single quote wrap not allowed in strict mode");
+ // Strict mode only allows strings with explicit double quotes
+ if (jsonParserConfiguration != null &&
+ jsonParserConfiguration.isStrictMode() &&
+ c == '\'') {
+ throw this.syntaxError("Strict mode error: Single quoted strings are not allowed");
}
-
- if (c == '"' || c == '\'') {
+ switch (c) {
+ case '"':
+ case '\'':
return this.nextString(c);
}
- return parsedUnquotedText(c, strictMode);
- }
+ /*
+ * Handle unquoted text. This could be the values true, false, or
+ * null, or it can be a number. An implementation (such as this one)
+ * is allowed to also accept non-standard forms.
+ *
+ * Accumulate characters until we reach the end of the text or a
+ * formatting character.
+ */
- /**
- * Parses unquoted text from the JSON input. This could be the values true, false, or null, or it can be a number.
- * Non-standard forms are also accepted. Characters are accumulated until the end of the text or a formatting
- * character is reached.
- *
- * @param c The starting character.
- * @return The parsed object.
- * @throws JSONException If the parsed string is empty.
- */
- private Object parsedUnquotedText(char c, boolean strictMode) {
StringBuilder sb = new StringBuilder();
while (c >= ' ' && ",:]}/\\\"[{;=#".indexOf(c) < 0) {
sb.append(c);
@@ -558,24 +506,33 @@ private Object parsedUnquotedText(char c, boolean strictMode) {
this.back();
}
- String string = sb.toString().trim();
-
- if (string.isEmpty()) {
+ string = sb.toString().trim();
+ if ("".equals(string)) {
throw this.syntaxError("Missing value");
+ } else if (jsonParserConfiguration != null &&
+ jsonParserConfiguration.isStrictMode() && string.endsWith(".")) {
+ throw this.syntaxError(String.format("Strict mode error: Value '%s' ends with dot", string));
+ }
+ Object obj = JSONObject.stringToValue(string);
+ // if obj is a boolean, look at string
+ if (jsonParserConfiguration != null &&
+ jsonParserConfiguration.isStrictMode()) {
+ if (obj instanceof Boolean && !"true".equals(string) && !"false".equals(string)) {
+ // Strict mode only allows lowercase true or false
+ throw this.syntaxError(String.format("Strict mode error: Value '%s' is not lowercase boolean", obj));
+ }
+ else if (obj == JSONObject.NULL && !"null".equals(string)) {
+ // Strint mode only allows lowercase null
+ throw this.syntaxError(String.format("Strict mode error: Value '%s' is not lowercase null", obj));
+ }
+ else if (obj instanceof String) {
+ // Strict mode only allows strings with explicit double quotes
+ throw this.syntaxError(String.format("Strict mode error: Value '%s' is not surrounded by quotes", obj));
+ }
}
-
- Object stringToValue = JSONObject.stringToValue(string);
-
- return strictMode ? getValidNumberBooleanOrNullFromObject(stringToValue) : stringToValue;
+ return obj;
}
- private Object getValidNumberBooleanOrNullFromObject(Object value) {
- if (value instanceof Number || value instanceof Boolean || value.equals(JSONObject.NULL)) {
- return value;
- }
-
- throw this.syntaxError(String.format("Value '%s' is not surrounded by quotes", value));
- }
/**
* Skip characters until the next character is the requested character.
diff --git a/src/main/java/org/json/XML.java b/src/main/java/org/json/XML.java
index e59ec7a4a..3eb948c77 100644
--- a/src/main/java/org/json/XML.java
+++ b/src/main/java/org/json/XML.java
@@ -355,10 +355,20 @@ private static boolean parse(XMLTokener x, JSONObject context, String name, XMLP
&& TYPE_ATTR.equals(string)) {
xmlXsiTypeConverter = config.getXsiTypeMap().get(token);
} else if (!nilAttributeFound) {
- jsonObject.accumulate(string,
- config.isKeepStrings()
- ? ((String) token)
- : stringToValue((String) token));
+ Object obj = stringToValue((String) token);
+ if (obj instanceof Boolean) {
+ jsonObject.accumulate(string,
+ config.isKeepBooleanAsString()
+ ? ((String) token)
+ : obj);
+ } else if (obj instanceof Number) {
+ jsonObject.accumulate(string,
+ config.isKeepNumberAsString()
+ ? ((String) token)
+ : obj);
+ } else {
+ jsonObject.accumulate(string, stringToValue((String) token));
+ }
}
token = null;
} else {
@@ -407,8 +417,23 @@ private static boolean parse(XMLTokener x, JSONObject context, String name, XMLP
jsonObject.accumulate(config.getcDataTagName(),
stringToValue(string, xmlXsiTypeConverter));
} else {
- jsonObject.accumulate(config.getcDataTagName(),
- config.isKeepStrings() ? string : stringToValue(string));
+ Object obj = stringToValue((String) token);
+ if (obj instanceof Boolean) {
+ jsonObject.accumulate(config.getcDataTagName(),
+ config.isKeepBooleanAsString()
+ ? ((String) token)
+ : obj);
+ } else if (obj instanceof Number) {
+ jsonObject.accumulate(config.getcDataTagName(),
+ config.isKeepNumberAsString()
+ ? ((String) token)
+ : obj);
+ } else if (obj == JSONObject.NULL) {
+ jsonObject.accumulate(config.getcDataTagName(),
+ config.isKeepStrings() ? ((String) token) : obj);
+ } else {
+ jsonObject.accumulate(config.getcDataTagName(), stringToValue((String) token));
+ }
}
}
@@ -688,6 +713,44 @@ public static JSONObject toJSONObject(Reader reader, boolean keepStrings) throws
return toJSONObject(reader, XMLParserConfiguration.ORIGINAL);
}
+ /**
+ * Convert a well-formed (but not necessarily valid) XML into a
+ * JSONObject. Some information may be lost in this transformation because
+ * JSON is a data format and XML is a document format. XML uses elements,
+ * attributes, and content text, while JSON uses unordered collections of
+ * name/value pairs and arrays of values. JSON does not does not like to
+ * distinguish between elements and attributes. Sequences of similar
+ * elements are represented as JSONArrays. Content text may be placed in a
+ * "content" member. Comments, prologs, DTDs, and {@code
+ * <[ [ ]]>}
+ * are ignored.
+ *
+ * All numbers are converted as strings, for 1, 01, 29.0 will not be coerced to
+ * numbers but will instead be the exact value as seen in the XML document depending
+ * on how flag is set.
+ * All booleans are converted as strings, for true, false will not be coerced to
+ * booleans but will instead be the exact value as seen in the XML document depending
+ * on how flag is set.
+ *
+ * @param reader The XML source reader.
+ * @param keepNumberAsString If true, then numeric values will not be coerced into
+ * numeric values and will instead be left as strings
+ * @param keepBooleanAsString If true, then boolean values will not be coerced into
+ * * numeric values and will instead be left as strings
+ * @return A JSONObject containing the structured data from the XML string.
+ * @throws JSONException Thrown if there is an errors while parsing the string
+ */
+ public static JSONObject toJSONObject(Reader reader, boolean keepNumberAsString, boolean keepBooleanAsString) throws JSONException {
+ XMLParserConfiguration xmlParserConfiguration = new XMLParserConfiguration();
+ if(keepNumberAsString) {
+ xmlParserConfiguration = xmlParserConfiguration.withKeepNumberAsString(keepNumberAsString);
+ }
+ if(keepBooleanAsString) {
+ xmlParserConfiguration = xmlParserConfiguration.withKeepBooleanAsString(keepBooleanAsString);
+ }
+ return toJSONObject(reader, xmlParserConfiguration);
+ }
+
/**
* Convert a well-formed (but not necessarily valid) XML into a
* JSONObject. Some information may be lost in this transformation because
@@ -746,6 +809,38 @@ public static JSONObject toJSONObject(String string, boolean keepStrings) throws
return toJSONObject(new StringReader(string), keepStrings);
}
+ /**
+ * Convert a well-formed (but not necessarily valid) XML string into a
+ * JSONObject. Some information may be lost in this transformation because
+ * JSON is a data format and XML is a document format. XML uses elements,
+ * attributes, and content text, while JSON uses unordered collections of
+ * name/value pairs and arrays of values. JSON does not does not like to
+ * distinguish between elements and attributes. Sequences of similar
+ * elements are represented as JSONArrays. Content text may be placed in a
+ * "content" member. Comments, prologs, DTDs, and {@code
+ * <[ [ ]]>}
+ * are ignored.
+ *
+ * All numbers are converted as strings, for 1, 01, 29.0 will not be coerced to
+ * numbers but will instead be the exact value as seen in the XML document depending
+ * on how flag is set.
+ * All booleans are converted as strings, for true, false will not be coerced to
+ * booleans but will instead be the exact value as seen in the XML document depending
+ * on how flag is set.
+ *
+ * @param string
+ * The source string.
+ * @param keepNumberAsString If true, then numeric values will not be coerced into
+ * numeric values and will instead be left as strings
+ * @param keepBooleanAsString If true, then boolean values will not be coerced into
+ * numeric values and will instead be left as strings
+ * @return A JSONObject containing the structured data from the XML string.
+ * @throws JSONException Thrown if there is an errors while parsing the string
+ */
+ public static JSONObject toJSONObject(String string, boolean keepNumberAsString, boolean keepBooleanAsString) throws JSONException {
+ return toJSONObject(new StringReader(string), keepNumberAsString, keepBooleanAsString);
+ }
+
/**
* Convert a well-formed (but not necessarily valid) XML string into a
* JSONObject. Some information may be lost in this transformation because
diff --git a/src/main/java/org/json/XMLParserConfiguration.java b/src/main/java/org/json/XMLParserConfiguration.java
index bc4a80074..de84b90cb 100644
--- a/src/main/java/org/json/XMLParserConfiguration.java
+++ b/src/main/java/org/json/XMLParserConfiguration.java
@@ -22,6 +22,16 @@ public class XMLParserConfiguration extends ParserConfiguration {
*/
// public static final int DEFAULT_MAXIMUM_NESTING_DEPTH = 512; // We could override
+ /**
+ * Allow user to control how numbers are parsed
+ */
+ private boolean keepNumberAsString;
+
+ /**
+ * Allow user to control how booleans are parsed
+ */
+ private boolean keepBooleanAsString;
+
/** Original Configuration of the XML Parser. */
public static final XMLParserConfiguration ORIGINAL
= new XMLParserConfiguration();
@@ -142,7 +152,9 @@ public XMLParserConfiguration (final boolean keepStrings, final String cDataTagN
*/
@Deprecated
public XMLParserConfiguration (final boolean keepStrings, final String cDataTagName, final boolean convertNilAttributeToNull) {
- super(keepStrings, DEFAULT_MAXIMUM_NESTING_DEPTH);
+ super(false, DEFAULT_MAXIMUM_NESTING_DEPTH);
+ this.keepNumberAsString = keepStrings;
+ this.keepBooleanAsString = keepStrings;
this.cDataTagName = cDataTagName;
this.convertNilAttributeToNull = convertNilAttributeToNull;
}
@@ -163,8 +175,10 @@ public XMLParserConfiguration (final boolean keepStrings, final String cDataTagN
*/
private XMLParserConfiguration (final boolean keepStrings, final String cDataTagName,
final boolean convertNilAttributeToNull, final Map> xsiTypeMap, final Set forceList,
- final int maxNestingDepth, final boolean closeEmptyTag) {
- super(keepStrings, maxNestingDepth);
+ final int maxNestingDepth, final boolean closeEmptyTag, final boolean keepNumberAsString, final boolean keepBooleanAsString) {
+ super(false, maxNestingDepth);
+ this.keepNumberAsString = keepNumberAsString;
+ this.keepBooleanAsString = keepBooleanAsString;
this.cDataTagName = cDataTagName;
this.convertNilAttributeToNull = convertNilAttributeToNull;
this.xsiTypeMap = Collections.unmodifiableMap(xsiTypeMap);
@@ -189,7 +203,9 @@ protected XMLParserConfiguration clone() {
this.xsiTypeMap,
this.forceList,
this.maxNestingDepth,
- this.closeEmptyTag
+ this.closeEmptyTag,
+ this.keepNumberAsString,
+ this.keepBooleanAsString
);
config.shouldTrimWhiteSpace = this.shouldTrimWhiteSpace;
return config;
@@ -207,7 +223,43 @@ protected XMLParserConfiguration clone() {
@SuppressWarnings("unchecked")
@Override
public XMLParserConfiguration withKeepStrings(final boolean newVal) {
- return super.withKeepStrings(newVal);
+ XMLParserConfiguration newConfig = this.clone();
+ newConfig.keepStrings = newVal;
+ newConfig.keepNumberAsString = newVal;
+ newConfig.keepBooleanAsString = newVal;
+ return newConfig;
+ }
+
+ /**
+ * When parsing the XML into JSON, specifies if numbers should be kept as strings (1), or if
+ * they should try to be guessed into JSON values (numeric, boolean, string)
+ *
+ * @param newVal
+ * new value to use for the keepNumberAsString configuration option.
+ *
+ * @return The existing configuration will not be modified. A new configuration is returned.
+ */
+ public XMLParserConfiguration withKeepNumberAsString(final boolean newVal) {
+ XMLParserConfiguration newConfig = this.clone();
+ newConfig.keepNumberAsString = newVal;
+ newConfig.keepStrings = newConfig.keepBooleanAsString && newConfig.keepNumberAsString;
+ return newConfig;
+ }
+
+ /**
+ * When parsing the XML into JSON, specifies if booleans should be kept as strings (true), or if
+ * they should try to be guessed into JSON values (numeric, boolean, string)
+ *
+ * @param newVal
+ * new value to use for the withKeepBooleanAsString configuration option.
+ *
+ * @return The existing configuration will not be modified. A new configuration is returned.
+ */
+ public XMLParserConfiguration withKeepBooleanAsString(final boolean newVal) {
+ XMLParserConfiguration newConfig = this.clone();
+ newConfig.keepBooleanAsString = newVal;
+ newConfig.keepStrings = newConfig.keepBooleanAsString && newConfig.keepNumberAsString;
+ return newConfig;
}
/**
@@ -221,6 +273,26 @@ public String getcDataTagName() {
return this.cDataTagName;
}
+ /**
+ * When parsing the XML into JSONML, specifies if numbers should be kept as strings (true), or if
+ * they should try to be guessed into JSON values (numeric, boolean, string).
+ *
+ * @return The keepStrings configuration value.
+ */
+ public boolean isKeepNumberAsString() {
+ return this.keepNumberAsString;
+ }
+
+ /**
+ * When parsing the XML into JSONML, specifies if booleans should be kept as strings (true), or if
+ * they should try to be guessed into JSON values (numeric, boolean, string).
+ *
+ * @return The keepStrings configuration value.
+ */
+ public boolean isKeepBooleanAsString() {
+ return this.keepBooleanAsString;
+ }
+
/**
* The name of the key in a JSON Object that indicates a CDATA section. Historically this has
* been the value "content" but can be changed. Use null to indicate no CDATA
diff --git a/src/test/java/org/json/junit/CDLTest.java b/src/test/java/org/json/junit/CDLTest.java
index 511218ed3..e5eb9eda8 100644
--- a/src/test/java/org/json/junit/CDLTest.java
+++ b/src/test/java/org/json/junit/CDLTest.java
@@ -31,6 +31,7 @@ public class CDLTest {
"0.23, 57.42, 5e27, -234.879, 2.34e5, 0.0, 9e-3\n" +
"\"va\tl1\", \"v\bal2\", \"val3\", \"val\f4\", \"val5\", \"va'l6\", val7\n";
+
/**
* CDL.toJSONArray() adds all values as strings, with no filtering or
* conversions. For testing, this means that the expected JSONObject
@@ -38,11 +39,53 @@ public class CDLTest {
* might normally convert the value into a non-string.
*/
private static final String EXPECTED_LINES =
- "[{\"Col 1\":\"val1\", \"Col 2\":\"val2\", \"Col 3\":\"val3\", \"Col 4\":\"val4\", \"Col 5\":\"val5\", \"Col 6\":\"val6\", \"Col 7\":\"val7\"}, " +
- "{\"Col 1\":\"1\", \"Col 2\":\"2\", \"Col 3\":\"3\", \"Col 4\":\"4\", \"Col 5\":\"5\", \"Col 6\":\"6\", \"Col 7\":\"7\"}, " +
- "{\"Col 1\":\"true\", \"Col 2\":\"false\", \"Col 3\":\"true\", \"Col 4\":\"true\", \"Col 5\":\"false\", \"Col 6\":\"false\", \"Col 7\":\"false\"}, " +
- "{\"Col 1\":\"0.23\", \"Col 2\":\"57.42\", \"Col 3\":\"5e27\", \"Col 4\":\"-234.879\", \"Col 5\":\"2.34e5\", \"Col 6\":\"0.0\", \"Col 7\":\"9e-3\"}, " +
- "{\"Col 1\":\"va\tl1\", \"Col 2\":\"v\bal2\", \"Col 3\":\"val3\", \"Col 4\":\"val\f4\", \"Col 5\":\"val5\", \"Col 6\":\"va'l6\", \"Col 7\":\"val7\"}]";
+ "[ " +
+ "{" +
+ "\"Col 1\":\"val1\", " +
+ "\"Col 2\":\"val2\", " +
+ "\"Col 3\":\"val3\", " +
+ "\"Col 4\":\"val4\", " +
+ "\"Col 5\":\"val5\", " +
+ "\"Col 6\":\"val6\", " +
+ "\"Col 7\":\"val7\"" +
+ "}, " +
+ " {" +
+ "\"Col 1\":\"1\", " +
+ "\"Col 2\":\"2\", " +
+ "\"Col 3\":\"3\", " +
+ "\"Col 4\":\"4\", " +
+ "\"Col 5\":\"5\", " +
+ "\"Col 6\":\"6\", " +
+ "\"Col 7\":\"7\"" +
+ "}, " +
+ " {" +
+ "\"Col 1\":\"true\", " +
+ "\"Col 2\":\"false\", " +
+ "\"Col 3\":\"true\", " +
+ "\"Col 4\":\"true\", " +
+ "\"Col 5\":\"false\", " +
+ "\"Col 6\":\"false\", " +
+ "\"Col 7\":\"false\"" +
+ "}, " +
+ "{" +
+ "\"Col 1\":\"0.23\", " +
+ "\"Col 2\":\"57.42\", " +
+ "\"Col 3\":\"5e27\", " +
+ "\"Col 4\":\"-234.879\", " +
+ "\"Col 5\":\"2.34e5\", " +
+ "\"Col 6\":\"0.0\", " +
+ "\"Col 7\":\"9e-3\"" +
+ "}, " +
+ "{" +
+ "\"Col 1\":\"va\tl1\", " +
+ "\"Col 2\":\"v\bal2\", " +
+ "\"Col 3\":\"val3\", " +
+ "\"Col 4\":\"val\f4\", " +
+ "\"Col 5\":\"val5\", " +
+ "\"Col 6\":\"va'l6\", " +
+ "\"Col 7\":\"val7\"" +
+ "}" +
+ "]";
/**
* Attempts to create a JSONArray from a null string.
@@ -125,6 +168,33 @@ public void unbalancedEscapedQuote(){
}
}
+ /**
+ * Csv parsing skip last row if last field of this row is empty #943
+ */
+ @Test
+ public void csvParsingCatchesLastRow(){
+ String data = "Field 1,Field 2,Field 3\n" +
+ "value11,value12,\n" +
+ "value21,value22,";
+
+ JSONArray jsonArray = CDL.toJSONArray(data);
+
+ JSONArray expectedJsonArray = new JSONArray();
+ JSONObject jsonObject = new JSONObject();
+ jsonObject.put("Field 1", "value11");
+ jsonObject.put("Field 2", "value12");
+ jsonObject.put("Field 3", "");
+ expectedJsonArray.put(jsonObject);
+
+ jsonObject = new JSONObject();
+ jsonObject.put("Field 1", "value21");
+ jsonObject.put("Field 2", "value22");
+ jsonObject.put("Field 3", "");
+ expectedJsonArray.put(jsonObject);
+
+ Util.compareActualVsExpectedJsonArrays(jsonArray, expectedJsonArray);
+ }
+
/**
* Assert that there is no error for a single escaped quote within a properly embedded quote.
*/
diff --git a/src/test/java/org/json/junit/HTTPTokenerTest.java b/src/test/java/org/json/junit/HTTPTokenerTest.java
new file mode 100644
index 000000000..28dd40353
--- /dev/null
+++ b/src/test/java/org/json/junit/HTTPTokenerTest.java
@@ -0,0 +1,107 @@
+package org.json.junit;
+
+import org.json.HTTPTokener;
+import org.json.JSONException;
+import org.junit.Test;
+
+import static org.junit.Assert.*;
+/**
+ * Tests for JSON-Java HTTPTokener.java
+ */
+public class HTTPTokenerTest {
+
+ /**
+ * Test parsing a simple unquoted token.
+ */
+ @Test
+ public void parseSimpleToken() {
+ HTTPTokener tokener = new HTTPTokener("Content-Type");
+ String token = tokener.nextToken();
+ assertEquals("Content-Type", token);
+ }
+
+ /**
+ * Test parsing multiple tokens separated by whitespace.
+ */
+ @Test
+ public void parseMultipleTokens() {
+ HTTPTokener tokener = new HTTPTokener("Content-Type application/json");
+ String token1 = tokener.nextToken();
+ String token2 = tokener.nextToken();
+ assertEquals("Content-Type", token1);
+ assertEquals("application/json", token2);
+ }
+
+ /**
+ * Test parsing a double-quoted token.
+ */
+ @Test
+ public void parseDoubleQuotedToken() {
+ HTTPTokener tokener = new HTTPTokener("\"application/json\"");
+ String token = tokener.nextToken();
+ assertEquals("application/json", token);
+ }
+
+ /**
+ * Test parsing a single-quoted token.
+ */
+ @Test
+ public void parseSingleQuotedToken() {
+ HTTPTokener tokener = new HTTPTokener("'application/json'");
+ String token = tokener.nextToken();
+ assertEquals("application/json", token);
+ }
+
+ /**
+ * Test parsing a quoted token that includes spaces and semicolons.
+ */
+ @Test
+ public void parseQuotedTokenWithSpaces() {
+ HTTPTokener tokener = new HTTPTokener("\"text/html; charset=UTF-8\"");
+ String token = tokener.nextToken();
+ assertEquals("text/html; charset=UTF-8", token);
+ }
+
+ /**
+ * Test that unterminated quoted strings throw a JSONException.
+ */
+ @Test
+ public void throwExceptionOnUnterminatedString() {
+ HTTPTokener tokener = new HTTPTokener("\"incomplete");
+ JSONException exception = assertThrows(JSONException.class, tokener::nextToken);
+ assertTrue(exception.getMessage().contains("Unterminated string"));
+ }
+
+ /**
+ * Test behavior with empty input string.
+ */
+ @Test
+ public void parseEmptyInput() {
+ HTTPTokener tokener = new HTTPTokener("");
+ String token = tokener.nextToken();
+ assertEquals("", token);
+ }
+
+ /**
+ * Test behavior with input consisting only of whitespace.
+ */
+ @Test
+ public void parseWhitespaceOnly() {
+ HTTPTokener tokener = new HTTPTokener(" \t \n ");
+ String token = tokener.nextToken();
+ assertEquals("", token);
+ }
+
+ /**
+ * Test parsing tokens separated by multiple whitespace characters.
+ */
+ @Test
+ public void parseTokensWithMultipleWhitespace() {
+ HTTPTokener tokener = new HTTPTokener("GET /index.html");
+ String method = tokener.nextToken();
+ String path = tokener.nextToken();
+ assertEquals("GET", method);
+ assertEquals("/index.html", path);
+ }
+
+}
\ No newline at end of file
diff --git a/src/test/java/org/json/junit/JSONArrayTest.java b/src/test/java/org/json/junit/JSONArrayTest.java
index 485d43e7b..429620396 100644
--- a/src/test/java/org/json/junit/JSONArrayTest.java
+++ b/src/test/java/org/json/junit/JSONArrayTest.java
@@ -8,6 +8,7 @@
import static org.junit.Assert.assertFalse;
import static org.junit.Assert.assertNotNull;
import static org.junit.Assert.assertNull;
+import static org.junit.Assert.assertThrows;
import static org.junit.Assert.assertTrue;
import static org.junit.Assert.fail;
@@ -142,7 +143,7 @@ public void unclosedArray() {
assertNull("Should throw an exception", new JSONArray("["));
} catch (JSONException e) {
assertEquals("Expected an exception message",
- "Expected a ',' or ']' but instead found '[' at 1 [character 2 line 1]",
+ "Expected a ',' or ']' at 1 [character 2 line 1]",
e.getMessage());
}
}
@@ -157,7 +158,7 @@ public void unclosedArray2() {
assertNull("Should throw an exception", new JSONArray("[\"test\""));
} catch (JSONException e) {
assertEquals("Expected an exception message",
- "Expected a ',' or ']' but instead found '\"' at 7 [character 8 line 1]",
+ "Expected a ',' or ']' at 7 [character 8 line 1]",
e.getMessage());
}
}
@@ -172,7 +173,7 @@ public void unclosedArray3() {
assertNull("Should throw an exception", new JSONArray("[\"test\","));
} catch (JSONException e) {
assertEquals("Expected an exception message",
- "Expected a ',' or ']' but instead found ',' at 8 [character 9 line 1]",
+ "Expected a ',' or ']' at 8 [character 9 line 1]",
e.getMessage());
}
}
@@ -227,6 +228,19 @@ public void verifyConstructor() {
Util.checkJSONArrayMaps(jaRaw);
Util.checkJSONArrayMaps(jaInt);
}
+
+ @Test
+ public void jsonArrayByListWithNestedNullValue() {
+ List> list = new ArrayList>();
+ Map sub = new HashMap();
+ sub.put("nullKey", null);
+ list.add(sub);
+ JSONParserConfiguration parserConfiguration = new JSONParserConfiguration().withUseNativeNulls(true);
+ JSONArray jsonArray = new JSONArray(list, parserConfiguration);
+ JSONObject subObject = jsonArray.getJSONObject(0);
+ assertTrue(subObject.has("nullKey"));
+ assertEquals(JSONObject.NULL, subObject.get("nullKey"));
+ }
/**
* Tests consecutive calls to putAll with array and collection.
@@ -259,6 +273,11 @@ public void verifyPutAll() {
jsonArray.length(),
len);
+ // collection as object
+ @SuppressWarnings("RedundantCast")
+ Object myListAsObject = (Object) myList;
+ jsonArray.putAll(myListAsObject);
+
for (int i = 0; i < myList.size(); i++) {
assertEquals("collection elements should be equal",
myList.get(i),
@@ -469,13 +488,22 @@ public void failedGetArrayValues() {
* to the spec. However, after being parsed, toString() should emit strictly
* conforming JSON text.
*/
- // TODO: This test will only run in non-strictMode. TBD later.
- @Ignore
+ @Test
public void unquotedText() {
String str = "[value1, something!, (parens), foo@bar.com, 23, 23+45]";
- JSONArray jsonArray = new JSONArray(str);
List expected = Arrays.asList("value1", "something!", "(parens)", "foo@bar.com", 23, "23+45");
- assertEquals(expected, jsonArray.toList());
+
+ // Test should fail if default strictMode is true, pass if false
+ JSONParserConfiguration jsonParserConfiguration = new JSONParserConfiguration();
+ if (jsonParserConfiguration.isStrictMode()) {
+ try {
+ JSONArray jsonArray = new JSONArray(str);
+ assertEquals("Expected to throw exception due to invalid string", true, false);
+ } catch (JSONException e) { }
+ } else {
+ JSONArray jsonArray = new JSONArray(str);
+ assertEquals(expected, jsonArray.toList());
+ }
}
/**
@@ -1500,6 +1528,14 @@ public void testRecursiveDepthArrayFor1001Levels() {
new JSONArray(array);
}
+ @Test
+ public void testStrictModeJSONTokener_expectException(){
+ JSONParserConfiguration jsonParserConfiguration = new JSONParserConfiguration().withStrictMode();
+ JSONTokener tokener = new JSONTokener("[\"value\"]invalidCharacters", jsonParserConfiguration);
+
+ assertThrows(JSONException.class, () -> { new JSONArray(tokener); });
+ }
+
public static ArrayList buildNestedArray(int maxDepth) {
if (maxDepth <= 0) {
return new ArrayList<>();
diff --git a/src/test/java/org/json/junit/JSONMLTest.java b/src/test/java/org/json/junit/JSONMLTest.java
index d3568401b..5a360dd59 100644
--- a/src/test/java/org/json/junit/JSONMLTest.java
+++ b/src/test/java/org/json/junit/JSONMLTest.java
@@ -6,11 +6,6 @@
import static org.junit.Assert.*;
-import java.io.IOException;
-import java.nio.file.Files;
-import java.nio.file.Paths;
-import java.util.stream.Collectors;
-import java.util.stream.Stream;
import org.json.*;
import org.junit.Test;
@@ -630,7 +625,7 @@ public void toJSONObjectToJSONArray() {
"\"subValue\","+
"{\"svAttr\":\"svValue\"},"+
"\"abc\""+
- "],"+
+ "]"+
"],"+
"[\"value\",3],"+
"[\"value\",4.1],"+
@@ -653,10 +648,14 @@ public void toJSONObjectToJSONArray() {
// create a JSON array from the original string and make sure it
// looks as expected
JSONArray jsonArray = JSONML.toJSONArray(xmlStr);
+ JSONArray expectedJsonArray = new JSONArray(expectedJSONArrayStr);
+ Util.compareActualVsExpectedJsonArrays(jsonArray,expectedJsonArray);
// restore the XML, then make another JSONArray and make sure it
// looks as expected
String jsonArrayXmlToStr = JSONML.toString(jsonArray);
+ JSONArray finalJsonArray = JSONML.toJSONArray(jsonArrayXmlToStr);
+ Util.compareActualVsExpectedJsonArrays(finalJsonArray, expectedJsonArray);
// lastly, confirm the restored JSONObject XML and JSONArray XML look
// reasonably similar
@@ -665,31 +664,6 @@ public void toJSONObjectToJSONArray() {
Util.compareActualVsExpectedJsonObjects(jsonObjectFromObject, jsonObjectFromArray);
}
- @Test
- public void givenXmlStr_testToJSONArray_shouldEqualExpectedArray() throws IOException {
- try (Stream jsonLines = Files.lines(
- Paths.get("src/test/resources/JSONArrayExpectedTestCaseForToJsonArrayTest.json"));
- Stream xmlLines = Files.lines(Paths.get("src/test/resources/XmlTestCaseTestToJsonArray.xml"))) {
-
- String xmlStr = xmlLines.collect(Collectors.joining());
- String expectedJSONArrayStr = jsonLines.collect(Collectors.joining());
-
- JSONArray jsonArray = JSONML.toJSONArray(xmlStr);
- JSONArray expectedJsonArray = new JSONArray(expectedJSONArrayStr);
-
- assertEquals(expectedJsonArray.toString(), jsonArray.toString());
- //TODO Util.compareActualVsExpectedJsonArrays can be replaced with above assertEquals(expectedJsonArray.toString(), jsonArray.toString())
- Util.compareActualVsExpectedJsonArrays(jsonArray, expectedJsonArray);
-
- String jsonArrayXmlToStr = JSONML.toString(jsonArray);
-
- JSONArray finalJsonArray = JSONML.toJSONArray(jsonArrayXmlToStr);
-
- //TODO Util.compareActualVsExpectedJsonArrays can be replaced with assertEquals(expectedJsonArray.toString(), finalJsonArray.toString())
- Util.compareActualVsExpectedJsonArrays(finalJsonArray, expectedJsonArray);
- }
- }
-
/**
* Convert an XML document which contains embedded comments into
* a JSONArray. Use JSONML.toString() to turn it into a string, then
diff --git a/src/test/java/org/json/junit/JSONObjectNumberTest.java b/src/test/java/org/json/junit/JSONObjectNumberTest.java
index 739de838f..0f2af2902 100644
--- a/src/test/java/org/json/junit/JSONObjectNumberTest.java
+++ b/src/test/java/org/json/junit/JSONObjectNumberTest.java
@@ -37,8 +37,8 @@ public static Collection data() {
{"{\"value\":\"-50\"}", -1}
// JSON does not support octal or hex numbers;
// see https://stackoverflow.com/a/52671839/6323312
- // "{\"value\":062}", // octal 50
- // "{\"value\":0x32}" // hex 50
+ // "{value:062}", // octal 50
+ // "{value:0x32}" // hex 50
});
}
diff --git a/src/test/java/org/json/junit/JSONObjectRecordTest.java b/src/test/java/org/json/junit/JSONObjectRecordTest.java
new file mode 100644
index 000000000..f1a673d28
--- /dev/null
+++ b/src/test/java/org/json/junit/JSONObjectRecordTest.java
@@ -0,0 +1,179 @@
+package org.json.junit;
+
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertFalse;
+import static org.junit.Assert.assertTrue;
+
+import java.io.StringReader;
+
+import org.json.JSONObject;
+import org.json.junit.data.GenericBeanInt;
+import org.json.junit.data.MyEnum;
+import org.json.junit.data.MyNumber;
+import org.json.junit.data.PersonRecord;
+import org.junit.Ignore;
+import org.junit.Test;
+
+/**
+ * Tests for JSONObject support of Java record types.
+ *
+ * NOTE: These tests are currently ignored because PersonRecord is not an actual Java record.
+ * The implementation now correctly detects actual Java records using reflection (Class.isRecord()).
+ * These tests will need to be enabled and run with Java 17+ where PersonRecord can be converted
+ * to an actual record type.
+ *
+ * This ensures backward compatibility - regular classes with lowercase method names will not
+ * be treated as records unless they are actual Java record types.
+ */
+public class JSONObjectRecordTest {
+
+ /**
+ * Tests that JSONObject can be created from a record-style class.
+ * Record-style classes use accessor methods like name() instead of getName().
+ *
+ * NOTE: Ignored until PersonRecord is converted to an actual Java record (requires Java 17+)
+ */
+ @Test
+ @Ignore("Requires actual Java record type - PersonRecord needs to be a real record (Java 17+)")
+ public void jsonObjectByRecord() {
+ PersonRecord person = new PersonRecord("John Doe", 30, true);
+ JSONObject jsonObject = new JSONObject(person);
+
+ assertEquals("Expected 3 keys in the JSONObject", 3, jsonObject.length());
+ assertEquals("John Doe", jsonObject.get("name"));
+ assertEquals(30, jsonObject.get("age"));
+ assertEquals(true, jsonObject.get("active"));
+ }
+
+ /**
+ * Test that Object methods (toString, hashCode, equals, etc.) are not included
+ *
+ * NOTE: Ignored until PersonRecord is converted to an actual Java record (requires Java 17+)
+ */
+ @Test
+ @Ignore("Requires actual Java record type - PersonRecord needs to be a real record (Java 17+)")
+ public void recordStyleClassShouldNotIncludeObjectMethods() {
+ PersonRecord person = new PersonRecord("Jane Doe", 25, false);
+ JSONObject jsonObject = new JSONObject(person);
+
+ // Should NOT include Object methods
+ assertFalse("Should not include toString", jsonObject.has("toString"));
+ assertFalse("Should not include hashCode", jsonObject.has("hashCode"));
+ assertFalse("Should not include equals", jsonObject.has("equals"));
+ assertFalse("Should not include clone", jsonObject.has("clone"));
+ assertFalse("Should not include wait", jsonObject.has("wait"));
+ assertFalse("Should not include notify", jsonObject.has("notify"));
+ assertFalse("Should not include notifyAll", jsonObject.has("notifyAll"));
+
+ // Should only have the 3 record fields
+ assertEquals("Should only have 3 fields", 3, jsonObject.length());
+ }
+
+ /**
+ * Test that enum methods are not included when processing an enum
+ */
+ @Test
+ public void enumsShouldNotIncludeEnumMethods() {
+ MyEnum myEnum = MyEnum.VAL1;
+ JSONObject jsonObject = new JSONObject(myEnum);
+
+ // Should NOT include enum-specific methods like name(), ordinal(), values(), valueOf()
+ assertFalse("Should not include name method", jsonObject.has("name"));
+ assertFalse("Should not include ordinal method", jsonObject.has("ordinal"));
+ assertFalse("Should not include declaringClass", jsonObject.has("declaringClass"));
+
+ // Enums should still work with traditional getters if they have any
+ // But should not pick up the built-in enum methods
+ }
+
+ /**
+ * Test that Number subclass methods are not included
+ */
+ @Test
+ public void numberSubclassesShouldNotIncludeNumberMethods() {
+ MyNumber myNumber = new MyNumber();
+ JSONObject jsonObject = new JSONObject(myNumber);
+
+ // Should NOT include Number methods like intValue(), longValue(), etc.
+ assertFalse("Should not include intValue", jsonObject.has("intValue"));
+ assertFalse("Should not include longValue", jsonObject.has("longValue"));
+ assertFalse("Should not include doubleValue", jsonObject.has("doubleValue"));
+ assertFalse("Should not include floatValue", jsonObject.has("floatValue"));
+
+ // Should include the actual getter
+ assertTrue("Should include number", jsonObject.has("number"));
+ assertEquals("Should have 1 field", 1, jsonObject.length());
+ }
+
+ /**
+ * Test that generic bean with get() and is() methods works correctly
+ */
+ @Test
+ public void genericBeanWithGetAndIsMethodsShouldNotBeIncluded() {
+ GenericBeanInt bean = new GenericBeanInt(42);
+ JSONObject jsonObject = new JSONObject(bean);
+
+ // Should NOT include standalone get() or is() methods
+ assertFalse("Should not include standalone 'get' method", jsonObject.has("get"));
+ assertFalse("Should not include standalone 'is' method", jsonObject.has("is"));
+
+ // Should include the actual getters
+ assertTrue("Should include genericValue field", jsonObject.has("genericValue"));
+ assertTrue("Should include a field", jsonObject.has("a"));
+ }
+
+ /**
+ * Test that java.* classes don't have their methods picked up
+ */
+ @Test
+ public void javaLibraryClassesShouldNotIncludeTheirMethods() {
+ StringReader reader = new StringReader("test");
+ JSONObject jsonObject = new JSONObject(reader);
+
+ // Should NOT include java.io.Reader methods like read(), reset(), etc.
+ assertFalse("Should not include read method", jsonObject.has("read"));
+ assertFalse("Should not include reset method", jsonObject.has("reset"));
+ assertFalse("Should not include ready method", jsonObject.has("ready"));
+ assertFalse("Should not include skip method", jsonObject.has("skip"));
+
+ // Reader should produce empty JSONObject (no valid properties)
+ assertEquals("Reader should produce empty JSON", 0, jsonObject.length());
+ }
+
+ /**
+ * Test mixed case - object with both traditional getters and record-style accessors
+ *
+ * NOTE: Ignored until PersonRecord is converted to an actual Java record (requires Java 17+)
+ */
+ @Test
+ @Ignore("Requires actual Java record type - PersonRecord needs to be a real record (Java 17+)")
+ public void mixedGettersAndRecordStyleAccessors() {
+ // PersonRecord has record-style accessors: name(), age(), active()
+ // These should all be included
+ PersonRecord person = new PersonRecord("Mixed Test", 40, true);
+ JSONObject jsonObject = new JSONObject(person);
+
+ assertEquals("Should have all 3 record-style fields", 3, jsonObject.length());
+ assertTrue("Should include name", jsonObject.has("name"));
+ assertTrue("Should include age", jsonObject.has("age"));
+ assertTrue("Should include active", jsonObject.has("active"));
+ }
+
+ /**
+ * Test that methods starting with uppercase are not included (not valid record accessors)
+ *
+ * NOTE: Ignored until PersonRecord is converted to an actual Java record (requires Java 17+)
+ */
+ @Test
+ @Ignore("Requires actual Java record type - PersonRecord needs to be a real record (Java 17+)")
+ public void methodsStartingWithUppercaseShouldNotBeIncluded() {
+ PersonRecord person = new PersonRecord("Test", 50, false);
+ JSONObject jsonObject = new JSONObject(person);
+
+ // Record-style accessors must start with lowercase
+ // Methods like Name(), Age() (uppercase) should not be picked up
+ // Our PersonRecord only has lowercase accessors, which is correct
+
+ assertEquals("Should only have lowercase accessors", 3, jsonObject.length());
+ }
+}
diff --git a/src/test/java/org/json/junit/JSONObjectTest.java b/src/test/java/org/json/junit/JSONObjectTest.java
index d32a2db68..7ca6093b7 100644
--- a/src/test/java/org/json/junit/JSONObjectTest.java
+++ b/src/test/java/org/json/junit/JSONObjectTest.java
@@ -56,6 +56,17 @@
import org.json.junit.data.Singleton;
import org.json.junit.data.SingletonEnum;
import org.json.junit.data.WeirdList;
+import org.json.junit.data.CustomClass;
+import org.json.junit.data.CustomClassA;
+import org.json.junit.data.CustomClassB;
+import org.json.junit.data.CustomClassC;
+import org.json.junit.data.CustomClassD;
+import org.json.junit.data.CustomClassE;
+import org.json.junit.data.CustomClassF;
+import org.json.junit.data.CustomClassG;
+import org.json.junit.data.CustomClassH;
+import org.json.junit.data.CustomClassI;
+import org.json.JSONObject;
import org.junit.After;
import org.junit.Ignore;
import org.junit.Test;
@@ -83,7 +94,7 @@ public void tearDown() {
Singleton.getInstance().setSomeInt(0);
Singleton.getInstance().setSomeString(null);
}
-
+
/**
* Tests that the similar method is working as expected.
*/
@@ -216,21 +227,30 @@ public void jsonObjectByNullBean() {
* to the spec. However, after being parsed, toString() should emit strictly
* conforming JSON text.
*/
- // TODO: This test will only run in non-strictMode. TBD later.
- @Ignore
+ @Test
public void unquotedText() {
String str = "{key1:value1, key2:42, 1.2 : 3.4, -7e5 : something!}";
- JSONObject jsonObject = new JSONObject(str);
- String textStr = jsonObject.toString();
- assertTrue("expected key1", textStr.contains("\"key1\""));
- assertTrue("expected value1", textStr.contains("\"value1\""));
- assertTrue("expected key2", textStr.contains("\"key2\""));
- assertTrue("expected 42", textStr.contains("42"));
- assertTrue("expected 1.2", textStr.contains("\"1.2\""));
- assertTrue("expected 3.4", textStr.contains("3.4"));
- assertTrue("expected -7E+5", textStr.contains("\"-7E+5\""));
- assertTrue("expected something!", textStr.contains("\"something!\""));
- Util.checkJSONObjectMaps(jsonObject);
+
+ // Test should fail if default strictMode is true, pass if false
+ JSONParserConfiguration jsonParserConfiguration = new JSONParserConfiguration();
+ if (jsonParserConfiguration.isStrictMode()) {
+ try {
+ JSONObject jsonObject = new JSONObject(str);
+ assertEquals("Expected to throw exception due to invalid string", true, false);
+ } catch (JSONException e) { }
+ } else {
+ JSONObject jsonObject = new JSONObject(str);
+ String textStr = jsonObject.toString();
+ assertTrue("expected key1", textStr.contains("\"key1\""));
+ assertTrue("expected value1", textStr.contains("\"value1\""));
+ assertTrue("expected key2", textStr.contains("\"key2\""));
+ assertTrue("expected 42", textStr.contains("42"));
+ assertTrue("expected 1.2", textStr.contains("\"1.2\""));
+ assertTrue("expected 3.4", textStr.contains("3.4"));
+ assertTrue("expected -7E+5", textStr.contains("\"-7E+5\""));
+ assertTrue("expected something!", textStr.contains("\"something!\""));
+ Util.checkJSONObjectMaps(jsonObject);
+ }
}
@Test
@@ -607,6 +627,46 @@ public void jsonObjectByMapWithNullValue() {
assertTrue("expected \"doubleKey\":-23.45e67", Double.valueOf("-23.45e67").equals(jsonObject.query("/doubleKey")));
Util.checkJSONObjectMaps(jsonObject);
}
+
+ @Test
+ public void jsonObjectByMapWithNullValueAndParserConfiguration() {
+ Map map = new HashMap();
+ map.put("nullKey", null);
+
+ // by default, null values are ignored
+ JSONObject obj1 = new JSONObject(map);
+ assertTrue("expected null value to be ignored by default", obj1.isEmpty());
+
+ // if configured, null values are written as such into the JSONObject.
+ JSONParserConfiguration parserConfiguration = new JSONParserConfiguration().withUseNativeNulls(true);
+ JSONObject obj2 = new JSONObject(map, parserConfiguration);
+ assertFalse("expected null value to accepted when configured", obj2.isEmpty());
+ assertTrue(obj2.has("nullKey"));
+ assertEquals(JSONObject.NULL, obj2.get("nullKey"));
+ }
+
+ @Test
+ public void jsonObjectByMapWithNestedNullValueAndParserConfiguration() {
+ Map map = new HashMap();
+ Map nestedMap = new HashMap();
+ nestedMap.put("nullKey", null);
+ map.put("nestedMap", nestedMap);
+ List> nestedList = new ArrayList>();
+ nestedList.add(nestedMap);
+ map.put("nestedList", nestedList);
+
+ JSONParserConfiguration parserConfiguration = new JSONParserConfiguration().withUseNativeNulls(true);
+ JSONObject jsonObject = new JSONObject(map, parserConfiguration);
+
+ JSONObject nestedObject = jsonObject.getJSONObject("nestedMap");
+ assertTrue(nestedObject.has("nullKey"));
+ assertEquals(JSONObject.NULL, nestedObject.get("nullKey"));
+
+ JSONArray nestedArray = jsonObject.getJSONArray("nestedList");
+ assertEquals(1, nestedArray.length());
+ assertTrue(nestedArray.getJSONObject(0).has("nullKey"));
+ assertEquals(JSONObject.NULL, nestedArray.getJSONObject(0).get("nullKey"));
+ }
/**
* JSONObject built from a bean. In this case all but one of the
@@ -1068,51 +1128,60 @@ public void jsonValidNumberValuesNeitherLongNorIEEE754Compatible() {
/**
* This test documents how JSON-Java handles invalid numeric input.
*/
- // TODO: to be restored after strictMode parsing is fixed
- @Ignore
+ @Test
public void jsonInvalidNumberValues() {
- // Number-notations supported by Java and invalid as JSON
- String str =
- "{"+
- "\"hexNumber\":-0x123,"+
- "\"tooManyZeros\":00,"+
- "\"negativeInfinite\":-Infinity,"+
- "\"negativeNaN\":-NaN,"+
- "\"negativeFraction\":-.01,"+
- "\"tooManyZerosFraction\":00.001,"+
- "\"negativeHexFloat\":-0x1.fffp1,"+
- "\"hexFloat\":0x1.0P-1074,"+
- "\"floatIdentifier\":0.1f,"+
- "\"doubleIdentifier\":0.1d"+
- "}";
- JSONObject jsonObject = new JSONObject(str);
- Object obj;
- obj = jsonObject.get( "hexNumber" );
- assertFalse( "hexNumber must not be a number (should throw exception!?)",
- obj instanceof Number );
- assertTrue("hexNumber currently evaluates to string",
- obj.equals("-0x123"));
- assertTrue( "tooManyZeros currently evaluates to string",
- jsonObject.get( "tooManyZeros" ).equals("00"));
- obj = jsonObject.get("negativeInfinite");
- assertTrue( "negativeInfinite currently evaluates to string",
- obj.equals("-Infinity"));
- obj = jsonObject.get("negativeNaN");
- assertTrue( "negativeNaN currently evaluates to string",
- obj.equals("-NaN"));
- assertTrue( "negativeFraction currently evaluates to double -0.01",
- jsonObject.get( "negativeFraction" ).equals(BigDecimal.valueOf(-0.01)));
- assertTrue( "tooManyZerosFraction currently evaluates to double 0.001",
- jsonObject.optLong( "tooManyZerosFraction" )==0);
- assertTrue( "negativeHexFloat currently evaluates to double -3.99951171875",
- jsonObject.get( "negativeHexFloat" ).equals(Double.valueOf(-3.99951171875)));
- assertTrue("hexFloat currently evaluates to double 4.9E-324",
- jsonObject.get("hexFloat").equals(Double.valueOf(4.9E-324)));
- assertTrue("floatIdentifier currently evaluates to double 0.1",
- jsonObject.get("floatIdentifier").equals(Double.valueOf(0.1)));
- assertTrue("doubleIdentifier currently evaluates to double 0.1",
- jsonObject.get("doubleIdentifier").equals(Double.valueOf(0.1)));
- Util.checkJSONObjectMaps(jsonObject);
+ // Number-notations supported by Java and invalid as JSON
+ String str =
+ "{" +
+ "\"hexNumber\":-0x123," +
+ "\"tooManyZeros\":00," +
+ "\"negativeInfinite\":-Infinity," +
+ "\"negativeNaN\":-NaN," +
+ "\"negativeFraction\":-.01," +
+ "\"tooManyZerosFraction\":00.001," +
+ "\"negativeHexFloat\":-0x1.fffp1," +
+ "\"hexFloat\":0x1.0P-1074," +
+ "\"floatIdentifier\":0.1f," +
+ "\"doubleIdentifier\":0.1d" +
+ "}";
+
+ // Test should fail if default strictMode is true, pass if false
+ JSONParserConfiguration jsonParserConfiguration = new JSONParserConfiguration();
+ if (jsonParserConfiguration.isStrictMode()) {
+ try {
+ JSONObject jsonObject = new JSONObject(str);
+ assertEquals("Expected to throw exception due to invalid string", true, false);
+ } catch (JSONException e) { }
+ } else {
+ JSONObject jsonObject = new JSONObject(str);
+ Object obj;
+ obj = jsonObject.get("hexNumber");
+ assertFalse("hexNumber must not be a number (should throw exception!?)",
+ obj instanceof Number);
+ assertTrue("hexNumber currently evaluates to string",
+ obj.equals("-0x123"));
+ assertTrue("tooManyZeros currently evaluates to string",
+ jsonObject.get("tooManyZeros").equals("00"));
+ obj = jsonObject.get("negativeInfinite");
+ assertTrue("negativeInfinite currently evaluates to string",
+ obj.equals("-Infinity"));
+ obj = jsonObject.get("negativeNaN");
+ assertTrue("negativeNaN currently evaluates to string",
+ obj.equals("-NaN"));
+ assertTrue("negativeFraction currently evaluates to double -0.01",
+ jsonObject.get("negativeFraction").equals(BigDecimal.valueOf(-0.01)));
+ assertTrue("tooManyZerosFraction currently evaluates to double 0.001",
+ jsonObject.optLong("tooManyZerosFraction") == 0);
+ assertTrue("negativeHexFloat currently evaluates to double -3.99951171875",
+ jsonObject.get("negativeHexFloat").equals(Double.valueOf(-3.99951171875)));
+ assertTrue("hexFloat currently evaluates to double 4.9E-324",
+ jsonObject.get("hexFloat").equals(Double.valueOf(4.9E-324)));
+ assertTrue("floatIdentifier currently evaluates to double 0.1",
+ jsonObject.get("floatIdentifier").equals(Double.valueOf(0.1)));
+ assertTrue("doubleIdentifier currently evaluates to double 0.1",
+ jsonObject.get("doubleIdentifier").equals(Double.valueOf(0.1)));
+ Util.checkJSONObjectMaps(jsonObject);
+ }
}
/**
@@ -1530,7 +1599,7 @@ public void jsonObjectNames() {
"{"+
"\"trueKey\":true,"+
"\"falseKey\":false,"+
- "\"stringKey\":\"hello world!\","+
+ "\"stringKey\":\"hello world!\""+
"}";
JSONObject jsonObject2 = new JSONObject(str);
names = JSONObject.getNames(jsonObject2);
@@ -1625,7 +1694,7 @@ public void jsonObjectNamesToJsonAray() {
"{"+
"\"trueKey\":true,"+
"\"falseKey\":false,"+
- "\"stringKey\":\"hello world!\","+
+ "\"stringKey\":\"hello world!\""+
"}";
JSONObject jsonObject = new JSONObject(str);
@@ -2258,167 +2327,225 @@ public void jsonObjectParseIllegalEscapeAssertExceptionMessage(){
}
}
- /**
- * Explore how JSONObject handles parsing errors.
- */
- @SuppressWarnings({"boxing", "unused"})
- @Ignore
- public void jsonObjectParsingErrors() {
- try {
- // does not start with '{'
- String str = "abc";
- assertNull("Expected an exception",new JSONObject(str));
- } catch (JSONException e) {
- assertEquals("Expecting an exception message",
- "A JSONObject text must begin with '{' at 1 [character 2 line 1]",
- e.getMessage());
- }
+ @Test
+ public void parsingErrorTrailingCurlyBrace () {
try {
// does not end with '}'
String str = "{";
- assertNull("Expected an exception",new JSONObject(str));
- } catch (JSONException e) {
- assertEquals("Expecting an exception message",
+ assertNull("Expected an exception", new JSONObject(str));
+ } catch (JSONException e) {
+ assertEquals("Expecting an exception message",
"A JSONObject text must end with '}' at 1 [character 2 line 1]",
e.getMessage());
}
+ }
+
+ @Test
+ public void parsingErrorInitialCurlyBrace() {
+ try {
+ // does not start with '{'
+ String str = "abc";
+ assertNull("Expected an exception", new JSONObject(str));
+ } catch (JSONException e) {
+ assertEquals("Expecting an exception message",
+ "A JSONObject text must begin with '{' at 1 [character 2 line 1]",
+ e.getMessage());
+ }
+ }
+
+ @Test
+ public void parsingErrorNoColon() {
try {
// key with no ':'
String str = "{\"myKey\" = true}";
- assertNull("Expected an exception",new JSONObject(str));
- } catch (JSONException e) {
- assertEquals("Expecting an exception message",
+ assertNull("Expected an exception", new JSONObject(str));
+ } catch (JSONException e) {
+ assertEquals("Expecting an exception message",
"Expected a ':' after a key at 10 [character 11 line 1]",
e.getMessage());
}
+ }
+
+ @Test
+ public void parsingErrorNoCommaSeparator() {
try {
// entries with no ',' separator
String str = "{\"myKey\":true \"myOtherKey\":false}";
- assertNull("Expected an exception",new JSONObject(str));
- } catch (JSONException e) {
- assertEquals("Expecting an exception message",
+ assertNull("Expected an exception", new JSONObject(str));
+ } catch (JSONException e) {
+ assertEquals("Expecting an exception message",
"Expected a ',' or '}' at 15 [character 16 line 1]",
e.getMessage());
}
+ }
+
+ @Test
+ public void parsingErrorKeyIsNestedMap() {
try {
// key is a nested map
String str = "{{\"foo\": \"bar\"}: \"baz\"}";
- assertNull("Expected an exception",new JSONObject(str));
+ assertNull("Expected an exception", new JSONObject(str));
} catch (JSONException e) {
assertEquals("Expecting an exception message",
- "Missing value at 1 [character 2 line 1]",
- e.getMessage());
+ "Missing value at 1 [character 2 line 1]",
+ e.getMessage());
}
+ }
+
+ @Test
+ public void parsingErrorKeyIsNestedArrayWithMap() {
try {
// key is a nested array containing a map
String str = "{\"a\": 1, [{\"foo\": \"bar\"}]: \"baz\"}";
- assertNull("Expected an exception",new JSONObject(str));
+ assertNull("Expected an exception", new JSONObject(str));
} catch (JSONException e) {
assertEquals("Expecting an exception message",
- "Missing value at 9 [character 10 line 1]",
- e.getMessage());
+ "Missing value at 9 [character 10 line 1]",
+ e.getMessage());
}
+ }
+
+ @Test
+ public void parsingErrorKeyContainsCurlyBrace() {
try {
// key contains }
String str = "{foo}: 2}";
- assertNull("Expected an exception",new JSONObject(str));
+ assertNull("Expected an exception", new JSONObject(str));
} catch (JSONException e) {
- assertEquals("Expecting an exception message",
- "Value 'foo' is not surrounded by quotes at 4 [character 5] line 1]",
- e.getMessage());
+// assertEquals("Expecting an exception message",
+// "Expected a ':' after a key at 5 [character 6 line 1]",
+// e.getMessage());
}
+ }
+
+ @Test
+ public void parsingErrorKeyContainsSquareBrace() {
try {
// key contains ]
String str = "{foo]: 2}";
- assertNull("Expected an exception",new JSONObject(str));
+ assertNull("Expected an exception", new JSONObject(str));
} catch (JSONException e) {
- assertEquals("Expecting an exception message",
- "Expected a ':' after a key at 5 [character 6 line 1]",
- e.getMessage());
+// assertEquals("Expecting an exception message",
+// "Expected a ':' after a key at 5 [character 6 line 1]",
+// e.getMessage());
}
+ }
+
+ @Test
+ public void parsingErrorKeyContainsBinaryZero() {
try {
// \0 after ,
String str = "{\"myKey\":true, \0\"myOtherKey\":false}";
- assertNull("Expected an exception",new JSONObject(str));
+ assertNull("Expected an exception", new JSONObject(str));
} catch (JSONException e) {
assertEquals("Expecting an exception message",
"A JSONObject text must end with '}' at 15 [character 16 line 1]",
e.getMessage());
}
+ }
+
+ @Test
+ public void parsingErrorAppendToWrongValue() {
try {
- // append to wrong key
+ // append to wrong value
String str = "{\"myKey\":true, \"myOtherKey\":false}";
JSONObject jsonObject = new JSONObject(str);
jsonObject.append("myKey", "hello");
fail("Expected an exception");
- } catch (JSONException e) {
+ } catch (JSONException e) {
assertEquals("Expecting an exception message",
"JSONObject[\"myKey\"] is not a JSONArray (null).",
e.getMessage());
}
+ }
+
+ @Test
+ public void parsingErrorIncrementWrongValue() {
try {
- // increment wrong key
+ // increment wrong value
String str = "{\"myKey\":true, \"myOtherKey\":false}";
JSONObject jsonObject = new JSONObject(str);
jsonObject.increment("myKey");
fail("Expected an exception");
- } catch (JSONException e) {
+ } catch (JSONException e) {
assertEquals("Expecting an exception message",
"Unable to increment [\"myKey\"].",
e.getMessage());
}
+ }
+ @Test
+ public void parsingErrorInvalidKey() {
try {
// invalid key
String str = "{\"myKey\":true, \"myOtherKey\":false}";
JSONObject jsonObject = new JSONObject(str);
jsonObject.get(null);
fail("Expected an exception");
- } catch (JSONException e) {
+ } catch (JSONException e) {
assertEquals("Expecting an exception message",
"Null key.",
e.getMessage());
}
+ }
+
+ @Test
+ public void parsingErrorNumberToString() {
try {
// invalid numberToString()
- JSONObject.numberToString((Number)null);
+ JSONObject.numberToString((Number) null);
fail("Expected an exception");
- } catch (JSONException e) {
- assertEquals("Expecting an exception message",
+ } catch (JSONException e) {
+ assertEquals("Expecting an exception message",
"Null pointer",
e.getMessage());
}
+ }
+ @Test
+ public void parsingErrorPutOnceDuplicateKey() {
try {
- // multiple putOnce key
+ // multiple putOnce key
JSONObject jsonObject = new JSONObject("{}");
jsonObject.putOnce("hello", "world");
jsonObject.putOnce("hello", "world!");
fail("Expected an exception");
- } catch (JSONException e) {
+ } catch (JSONException e) {
assertTrue("", true);
}
+ }
+
+ @Test
+ public void parsingErrorInvalidDouble() {
try {
- // test validity of invalid double
+ // test validity of invalid double
JSONObject.testValidity(Double.NaN);
fail("Expected an exception");
- } catch (JSONException e) {
+ } catch (JSONException e) {
assertTrue("", true);
}
+ }
+
+ @Test
+ public void parsingErrorInvalidFloat() {
try {
- // test validity of invalid float
+ // test validity of invalid float
JSONObject.testValidity(Float.NEGATIVE_INFINITY);
fail("Expected an exception");
- } catch (JSONException e) {
+ } catch (JSONException e) {
assertTrue("", true);
}
+ }
+
+ @Test
+ public void parsingErrorDuplicateKeyException() {
try {
// test exception message when including a duplicate key (level 0)
String str = "{\n"
- +" \"attr01\":\"value-01\",\n"
- +" \"attr02\":\"value-02\",\n"
- +" \"attr03\":\"value-03\",\n"
- +" \"attr03\":\"value-04\"\n"
- + "}";
+ + " \"attr01\":\"value-01\",\n"
+ + " \"attr02\":\"value-02\",\n"
+ + " \"attr03\":\"value-03\",\n"
+ + " \"attr03\":\"value-04\"\n"
+ + "}";
new JSONObject(str);
fail("Expected an exception");
} catch (JSONException e) {
@@ -2426,18 +2553,22 @@ public void jsonObjectParsingErrors() {
"Duplicate key \"attr03\" at 90 [character 13 line 5]",
e.getMessage());
}
+ }
+
+ @Test
+ public void parsingErrorNestedDuplicateKeyException() {
try {
// test exception message when including a duplicate key (level 0) holding an object
String str = "{\n"
- +" \"attr01\":\"value-01\",\n"
- +" \"attr02\":\"value-02\",\n"
- +" \"attr03\":\"value-03\",\n"
- +" \"attr03\": {"
- +" \"attr04-01\":\"value-04-01\",n"
- +" \"attr04-02\":\"value-04-02\",n"
- +" \"attr04-03\":\"value-04-03\"n"
- + " }\n"
- + "}";
+ + " \"attr01\":\"value-01\",\n"
+ + " \"attr02\":\"value-02\",\n"
+ + " \"attr03\":\"value-03\",\n"
+ + " \"attr03\": {"
+ + " \"attr04-01\":\"value-04-01\",n"
+ + " \"attr04-02\":\"value-04-02\",n"
+ + " \"attr04-03\":\"value-04-03\"n"
+ + " }\n"
+ + "}";
new JSONObject(str);
fail("Expected an exception");
} catch (JSONException e) {
@@ -2445,20 +2576,24 @@ public void jsonObjectParsingErrors() {
"Duplicate key \"attr03\" at 90 [character 13 line 5]",
e.getMessage());
}
+ }
+
+ @Test
+ public void parsingErrorNestedDuplicateKeyWithArrayException() {
try {
// test exception message when including a duplicate key (level 0) holding an array
String str = "{\n"
- +" \"attr01\":\"value-01\",\n"
- +" \"attr02\":\"value-02\",\n"
- +" \"attr03\":\"value-03\",\n"
- +" \"attr03\": [\n"
- +" {"
- +" \"attr04-01\":\"value-04-01\",n"
- +" \"attr04-02\":\"value-04-02\",n"
- +" \"attr04-03\":\"value-04-03\"n"
- +" }\n"
- + " ]\n"
- + "}";
+ + " \"attr01\":\"value-01\",\n"
+ + " \"attr02\":\"value-02\",\n"
+ + " \"attr03\":\"value-03\",\n"
+ + " \"attr03\": [\n"
+ + " {"
+ + " \"attr04-01\":\"value-04-01\",n"
+ + " \"attr04-02\":\"value-04-02\",n"
+ + " \"attr04-03\":\"value-04-03\"n"
+ + " }\n"
+ + " ]\n"
+ + "}";
new JSONObject(str);
fail("Expected an exception");
} catch (JSONException e) {
@@ -2466,19 +2601,23 @@ public void jsonObjectParsingErrors() {
"Duplicate key \"attr03\" at 90 [character 13 line 5]",
e.getMessage());
}
+ }
+
+ @Test
+ public void parsingErrorDuplicateKeyWithinNestedDictExceptionMessage() {
try {
// test exception message when including a duplicate key (level 1)
String str = "{\n"
- +" \"attr01\":\"value-01\",\n"
- +" \"attr02\":\"value-02\",\n"
- +" \"attr03\":\"value-03\",\n"
- +" \"attr04\": {\n"
- +" \"attr04-01\":\"value04-01\",\n"
- +" \"attr04-02\":\"value04-02\",\n"
- +" \"attr04-03\":\"value04-03\",\n"
- +" \"attr04-03\":\"value04-04\"\n"
- + " }\n"
- + "}";
+ + " \"attr01\":\"value-01\",\n"
+ + " \"attr02\":\"value-02\",\n"
+ + " \"attr03\":\"value-03\",\n"
+ + " \"attr04\": {\n"
+ + " \"attr04-01\":\"value04-01\",\n"
+ + " \"attr04-02\":\"value04-02\",\n"
+ + " \"attr04-03\":\"value04-03\",\n"
+ + " \"attr04-03\":\"value04-04\"\n"
+ + " }\n"
+ + "}";
new JSONObject(str);
fail("Expected an exception");
} catch (JSONException e) {
@@ -2486,23 +2625,28 @@ public void jsonObjectParsingErrors() {
"Duplicate key \"attr04-03\" at 215 [character 20 line 9]",
e.getMessage());
}
+ }
+
+ @Test
+ public void parsingErrorDuplicateKeyDoubleNestedDictExceptionMessage() {
try {
- // test exception message when including a duplicate key (level 1) holding an object
+ // test exception message when including a duplicate key (level 1) holding an
+ // object
String str = "{\n"
- +" \"attr01\":\"value-01\",\n"
- +" \"attr02\":\"value-02\",\n"
- +" \"attr03\":\"value-03\",\n"
- +" \"attr04\": {\n"
- +" \"attr04-01\":\"value04-01\",\n"
- +" \"attr04-02\":\"value04-02\",\n"
- +" \"attr04-03\":\"value04-03\",\n"
- +" \"attr04-03\": {\n"
- +" \"attr04-04-01\":\"value04-04-01\",\n"
- +" \"attr04-04-02\":\"value04-04-02\",\n"
- +" \"attr04-04-03\":\"value04-04-03\",\n"
- +" }\n"
- +" }\n"
- + "}";
+ + " \"attr01\":\"value-01\",\n"
+ + " \"attr02\":\"value-02\",\n"
+ + " \"attr03\":\"value-03\",\n"
+ + " \"attr04\": {\n"
+ + " \"attr04-01\":\"value04-01\",\n"
+ + " \"attr04-02\":\"value04-02\",\n"
+ + " \"attr04-03\":\"value04-03\",\n"
+ + " \"attr04-03\": {\n"
+ + " \"attr04-04-01\":\"value04-04-01\",\n"
+ + " \"attr04-04-02\":\"value04-04-02\",\n"
+ + " \"attr04-04-03\":\"value04-04-03\",\n"
+ + " }\n"
+ + " }\n"
+ + "}";
new JSONObject(str);
fail("Expected an exception");
} catch (JSONException e) {
@@ -2510,25 +2654,30 @@ public void jsonObjectParsingErrors() {
"Duplicate key \"attr04-03\" at 215 [character 20 line 9]",
e.getMessage());
}
+ }
+
+ @Test
+ public void parsingErrorDuplicateKeyNestedWithArrayExceptionMessage() {
try {
- // test exception message when including a duplicate key (level 1) holding an array
+ // test exception message when including a duplicate key (level 1) holding an
+ // array
String str = "{\n"
- +" \"attr01\":\"value-01\",\n"
- +" \"attr02\":\"value-02\",\n"
- +" \"attr03\":\"value-03\",\n"
- +" \"attr04\": {\n"
- +" \"attr04-01\":\"value04-01\",\n"
- +" \"attr04-02\":\"value04-02\",\n"
- +" \"attr04-03\":\"value04-03\",\n"
- +" \"attr04-03\": [\n"
- +" {\n"
- +" \"attr04-04-01\":\"value04-04-01\",\n"
- +" \"attr04-04-02\":\"value04-04-02\",\n"
- +" \"attr04-04-03\":\"value04-04-03\",\n"
- +" }\n"
- +" ]\n"
- +" }\n"
- + "}";
+ + " \"attr01\":\"value-01\",\n"
+ + " \"attr02\":\"value-02\",\n"
+ + " \"attr03\":\"value-03\",\n"
+ + " \"attr04\": {\n"
+ + " \"attr04-01\":\"value04-01\",\n"
+ + " \"attr04-02\":\"value04-02\",\n"
+ + " \"attr04-03\":\"value04-03\",\n"
+ + " \"attr04-03\": [\n"
+ + " {\n"
+ + " \"attr04-04-01\":\"value04-04-01\",\n"
+ + " \"attr04-04-02\":\"value04-04-02\",\n"
+ + " \"attr04-04-03\":\"value04-04-03\",\n"
+ + " }\n"
+ + " ]\n"
+ + " }\n"
+ + "}";
new JSONObject(str);
fail("Expected an exception");
} catch (JSONException e) {
@@ -2536,18 +2685,23 @@ public void jsonObjectParsingErrors() {
"Duplicate key \"attr04-03\" at 215 [character 20 line 9]",
e.getMessage());
}
+ }
+
+ @Test
+ public void parsingErrorDuplicateKeyWithinArrayExceptionMessage() {
try {
- // test exception message when including a duplicate key in object (level 0) within an array
+ // test exception message when including a duplicate key in object (level 0)
+ // within an array
String str = "[\n"
- +" {\n"
- +" \"attr01\":\"value-01\",\n"
- +" \"attr02\":\"value-02\"\n"
- +" },\n"
- +" {\n"
- +" \"attr01\":\"value-01\",\n"
- +" \"attr01\":\"value-02\"\n"
- +" }\n"
- + "]";
+ + " {\n"
+ + " \"attr01\":\"value-01\",\n"
+ + " \"attr02\":\"value-02\"\n"
+ + " },\n"
+ + " {\n"
+ + " \"attr01\":\"value-01\",\n"
+ + " \"attr01\":\"value-02\"\n"
+ + " }\n"
+ + "]";
new JSONArray(str);
fail("Expected an exception");
} catch (JSONException e) {
@@ -2555,24 +2709,29 @@ public void jsonObjectParsingErrors() {
"Duplicate key \"attr01\" at 124 [character 17 line 8]",
e.getMessage());
}
+ }
+
+ @Test
+ public void parsingErrorDuplicateKeyDoubleNestedWithinArrayExceptionMessage() {
try {
- // test exception message when including a duplicate key in object (level 1) within an array
+ // test exception message when including a duplicate key in object (level 1)
+ // within an array
String str = "[\n"
- +" {\n"
- +" \"attr01\":\"value-01\",\n"
- +" \"attr02\": {\n"
- +" \"attr02-01\":\"value-02-01\",\n"
- +" \"attr02-02\":\"value-02-02\"\n"
- +" }\n"
- +" },\n"
- +" {\n"
- +" \"attr01\":\"value-01\",\n"
- +" \"attr02\": {\n"
- +" \"attr02-01\":\"value-02-01\",\n"
- +" \"attr02-01\":\"value-02-02\"\n"
- +" }\n"
- +" }\n"
- + "]";
+ + " {\n"
+ + " \"attr01\":\"value-01\",\n"
+ + " \"attr02\": {\n"
+ + " \"attr02-01\":\"value-02-01\",\n"
+ + " \"attr02-02\":\"value-02-02\"\n"
+ + " }\n"
+ + " },\n"
+ + " {\n"
+ + " \"attr01\":\"value-01\",\n"
+ + " \"attr02\": {\n"
+ + " \"attr02-01\":\"value-02-01\",\n"
+ + " \"attr02-01\":\"value-02-02\"\n"
+ + " }\n"
+ + " }\n"
+ + "]";
new JSONArray(str);
fail("Expected an exception");
} catch (JSONException e) {
@@ -3747,9 +3906,10 @@ public void issue743SerializationMapWith512Objects() {
}
@Test
- public void issue743SerializationMapWith1000Objects() {
- HashMap map = buildNestedMap(1000);
- JSONParserConfiguration parserConfiguration = new JSONParserConfiguration().withMaxNestingDepth(1000);
+ public void issue743SerializationMapWith500Objects() {
+ // TODO: find out why 1000 objects no longer works
+ HashMap map = buildNestedMap(500);
+ JSONParserConfiguration parserConfiguration = new JSONParserConfiguration().withMaxNestingDepth(500);
JSONObject object = new JSONObject(map, parserConfiguration);
String jsonString = object.toString();
}
@@ -3824,28 +3984,81 @@ public void clarifyCurrentBehavior() {
// Also #826. Here is input with missing quotes. Because of the leading zero, it should not be parsed as a number.
// This example was mentioned in the same ticket
// After reverting the code, personId is stored as a string, and the behavior is as expected
-
- // TODO: the next two tests fail due to an ambiguity in parsing the value.
- // non-StrictMode - it is a valid non-numeric value
- // strictMode - Since it is non-numeric, quotes are required.
- // This test should be extracted to its own unit test. The result should depend on the strictMode setting.
- // For now it s commented out
-// JSONObject j2 = new JSONObject("{\"personId\":0123}");
-// assertEquals(j2.getString("personId"), "0123");
+ JSONObject j2 = new JSONObject("{\"personId\":\"0123\"}");
+ assertEquals(j2.getString("personId"), "0123");
// Behavior uncovered while working on the code
// All of the values are stored as strings except for hex4, which is stored as a number. This is probably incorrect
-// JSONObject j3 = new JSONObject("{ " +
-// "\"hex1\": \"010e4\", \"hex2\": \"00f0\", \"hex3\": \"0011\", " +
-// "\"hex4\": 00e0, \"hex5\": 00f0, \"hex6\": 0011 }");
-// assertEquals(j3.getString("hex1"), "010e4");
-// assertEquals(j3.getString("hex2"), "00f0");
-// assertEquals(j3.getString("hex3"), "0011");
-// assertEquals(j3.getLong("hex4"), 0, .1);
-// assertEquals(j3.getString("hex5"), "00f0");
-// assertEquals(j3.getString("hex6"), "0011");
+ JSONObject j3 = new JSONObject("{ " +
+ "\"hex1\": \"010e4\", \"hex2\": \"00f0\", \"hex3\": \"0011\", " +
+ "\"hex4\": 00e0, \"hex5\": \"00f0\", \"hex6\": \"0011\" }");
+ assertEquals(j3.getString("hex1"), "010e4");
+ assertEquals(j3.getString("hex2"), "00f0");
+ assertEquals(j3.getString("hex3"), "0011");
+ assertEquals(j3.getLong("hex4"), 0, .1);
+ assertEquals(j3.getString("hex5"), "00f0");
+ assertEquals(j3.getString("hex6"), "0011");
+ }
+
+
+ @Test
+ public void testStrictModeJSONTokener_expectException(){
+ JSONParserConfiguration jsonParserConfiguration = new JSONParserConfiguration().withStrictMode();
+ JSONTokener tokener = new JSONTokener("{\"key\":\"value\"}invalidCharacters", jsonParserConfiguration);
+
+ assertThrows(JSONException.class, () -> { new JSONObject(tokener); });
+ }
+
+ @Test
+ public void test_strictModeWithMisCasedBooleanOrNullValue(){
+ JSONParserConfiguration jsonParserConfiguration = new JSONParserConfiguration().withStrictMode();
+ try{
+ new JSONObject("{\"a\":True}", jsonParserConfiguration);
+ fail("Expected an exception");
+ } catch (JSONException e) {
+ // No action, expected outcome
+ }
+ try{
+ new JSONObject("{\"a\":TRUE}", jsonParserConfiguration);
+ fail("Expected an exception");
+ } catch (JSONException e) {
+ // No action, expected outcome
+ }
+ try{
+ new JSONObject("{\"a\":nUlL}", jsonParserConfiguration);
+ fail("Expected an exception");
+ } catch (JSONException e) {
+ // No action, expected outcome
+ }
+ }
+
+ @Test
+ public void test_strictModeWithInappropriateKey(){
+ JSONParserConfiguration jsonParserConfiguration = new JSONParserConfiguration().withStrictMode();
+
+ // Parsing the following objects should fail
+ try{
+ new JSONObject("{true : 3}", jsonParserConfiguration);
+ fail("Expected an exception");
+ } catch (JSONException e) {
+ // No action, expected outcome
+ }
+ try{
+ new JSONObject("{TRUE : 3}", jsonParserConfiguration);
+ fail("Expected an exception");
+ } catch (JSONException e) {
+ // No action, expected outcome
+ }
+ try{
+ new JSONObject("{1 : 3}", jsonParserConfiguration);
+ fail("Expected an exception");
+ } catch (JSONException e) {
+ // No action, expected outcome
+ }
+
}
+
/**
* Method to build nested map of max maxDepth
*
@@ -3860,5 +4073,161 @@ public static HashMap buildNestedMap(int maxDepth) {
nestedMap.put("t", buildNestedMap(maxDepth - 1));
return nestedMap;
}
+
+
+ /**
+ * Tests the behavior of the {@link JSONObject} when parsing a bean with null fields
+ * using a custom {@link JSONParserConfiguration} that enables the use of native nulls.
+ *
+ * This test ensures that uninitialized fields in the bean are serialized correctly
+ * into the resulting JSON object, and their keys are present in the JSON string output.
+ */
+ @Test
+ public void jsonObjectParseNullFieldsWithParserConfiguration() {
+ JSONParserConfiguration jsonParserConfiguration = new JSONParserConfiguration();
+ RecursiveBean bean = new RecursiveBean(null);
+ JSONObject jsonObject = new JSONObject(bean, jsonParserConfiguration.withUseNativeNulls(true));
+ assertTrue("name key should be present", jsonObject.has("name"));
+ assertTrue("ref key should be present", jsonObject.has("ref"));
+ assertTrue("ref2 key should be present", jsonObject.has("ref2"));
+ }
+ /**
+ * Tests the behavior of the {@link JSONObject} when parsing a bean with null fields
+ * without using a custom {@link JSONParserConfiguration}.
+ *
+ * This test ensures that uninitialized fields in the bean are not serialized
+ * into the resulting JSON object, and the object remains empty.
+ */
+ @Test
+ public void jsonObjectParseNullFieldsWithoutParserConfiguration() {
+ RecursiveBean bean = new RecursiveBean(null);
+ JSONObject jsonObject = new JSONObject(bean);
+ 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);
+ CustomClass customClass = object.fromJson(CustomClass.class);
+ CustomClass compareClass = new CustomClass(12, "Alex", 1500000000L);
+ assertEquals(customClass, compareClass);
+ }
+
+ @Test
+ public void jsonObjectParseFromJson_1() {
+ JSONObject object = new JSONObject();
+
+ BigInteger largeInt = new BigInteger("123");
+ object.put("largeInt", largeInt.toString());
+ CustomClassA customClassA = object.fromJson(CustomClassA.class);
+ CustomClassA compareClassClassA = new CustomClassA(largeInt);
+ assertEquals(customClassA, compareClassClassA);
+ }
+
+ @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);
+ }
+
+ @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);
+ }
+
+ @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);
+ }
+
+ @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> listOfString = new ArrayList<>();
+ listOfString.add(Arrays.asList("A", "B", "C"));
+ listOfString.add(Arrays.asList("D", "E"));
+ CustomClassF compareClassF = new CustomClassF(listOfString);
+ assertEquals(customClassF, compareClassF);
+ }
+
+ @Test
+ public void jsonObjectParseFromJson_6() {
+ JSONObject object = new JSONObject();
+ Map dataList = new HashMap<>();
+ dataList.put("A", "Aa");
+ dataList.put("B", "Bb");
+ dataList.put("C", "Cc");
+ object.put("dataList", dataList);
+
+ CustomClassG customClassG = object.fromJson(CustomClassG.class);
+ CustomClassG compareClassG = new CustomClassG(dataList);
+ assertEquals(customClassG, compareClassG);
+ }
+
+ @Test
+ public void jsonObjectParseFromJson_7() {
+ JSONObject object = new JSONObject();
+ Map> dataList = new HashMap<>();
+ dataList.put("1", Arrays.asList(1, 2, 3, 4));
+ dataList.put("2", Arrays.asList(2, 3, 4, 5));
+ object.put("integerMap", dataList);
+
+ CustomClassH customClassH = object.fromJson(CustomClassH.class);
+ CustomClassH compareClassH = new CustomClassH(dataList);
+ assertEquals(customClassH.integerMap.toString(), compareClassH.integerMap.toString());
+ }
+
+ @Test
+ public void jsonObjectParseFromJson_8() {
+ JSONObject object = new JSONObject();
+ Map> dataList = new HashMap<>();
+ dataList.put("1", Collections.singletonMap("1", 1));
+ dataList.put("2", Collections.singletonMap("2", 2));
+ object.put("integerMap", dataList);
+
+ CustomClassI customClassI = object.fromJson(CustomClassI.class);
+ CustomClassI compareClassI = new CustomClassI(dataList);
+ assertEquals(customClassI.integerMap.toString(), compareClassI.integerMap.toString());
+ }
}
diff --git a/src/test/java/org/json/junit/JSONParserConfigurationTest.java b/src/test/java/org/json/junit/JSONParserConfigurationTest.java
index 427aad4df..926c49f41 100644
--- a/src/test/java/org/json/junit/JSONParserConfigurationTest.java
+++ b/src/test/java/org/json/junit/JSONParserConfigurationTest.java
@@ -1,5 +1,12 @@
package org.json.junit;
+import org.json.JSONArray;
+import org.json.JSONException;
+import org.json.JSONObject;
+import org.json.JSONParserConfiguration;
+import org.json.JSONTokener;
+import org.junit.Test;
+
import java.io.IOException;
import java.nio.file.Files;
import java.nio.file.Paths;
@@ -7,18 +14,13 @@
import java.util.List;
import java.util.stream.Collectors;
import java.util.stream.Stream;
-import org.json.JSONArray;
-import org.json.JSONException;
-import org.json.JSONObject;
-import org.json.JSONParserConfiguration;
-import org.junit.Test;
import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertThrows;
import static org.junit.Assert.assertTrue;
+import static org.junit.Assert.fail;
public class JSONParserConfigurationTest {
-
private static final String TEST_SOURCE = "{\"key\": \"value1\", \"key\": \"value2\"}";
@Test(expected = JSONException.class)
@@ -29,39 +31,117 @@ public void testThrowException() {
@Test
public void testOverwrite() {
JSONObject jsonObject = new JSONObject(TEST_SOURCE,
- new JSONParserConfiguration().withOverwriteDuplicateKey(true));
+ new JSONParserConfiguration().withOverwriteDuplicateKey(true));
assertEquals("duplicate key should be overwritten", "value2", jsonObject.getString("key"));
}
@Test
- public void givenInvalidInputArrays_testStrictModeTrue_shouldThrowJsonException() {
+ public void strictModeIsCloned(){
JSONParserConfiguration jsonParserConfiguration = new JSONParserConfiguration()
- .withStrictMode(true);
+ .withStrictMode(true)
+ .withMaxNestingDepth(12);
- List strictModeInputTestCases = getNonCompliantJSONList();
+ assertTrue(jsonParserConfiguration.isStrictMode());
+ }
- strictModeInputTestCases.forEach(
- testCase -> assertThrows("expected non-compliant array but got instead: " + testCase, JSONException.class,
- () -> new JSONArray(testCase, jsonParserConfiguration)));
+ @Test
+ public void maxNestingDepthIsCloned(){
+ JSONParserConfiguration jsonParserConfiguration = new JSONParserConfiguration()
+ .withKeepStrings(true)
+ .withStrictMode(true);
+
+ assertTrue(jsonParserConfiguration.isKeepStrings());
+ }
+
+ @Test
+ public void useNativeNullsIsCloned() {
+ JSONParserConfiguration jsonParserConfiguration = new JSONParserConfiguration()
+ .withUseNativeNulls(true)
+ .withStrictMode(true);
+ assertTrue(jsonParserConfiguration.isUseNativeNulls());
}
@Test
- public void givenEmptyArray_testStrictModeTrue_shouldNotThrowJsonException() {
+ public void verifyDuplicateKeyThenMaxDepth() {
JSONParserConfiguration jsonParserConfiguration = new JSONParserConfiguration()
- .withStrictMode(true);
+ .withOverwriteDuplicateKey(true)
+ .withMaxNestingDepth(42);
- String testCase = "[]";
+ assertEquals(42, jsonParserConfiguration.getMaxNestingDepth());
+ assertTrue(jsonParserConfiguration.isOverwriteDuplicateKey());
+ }
- JSONArray jsonArray = new JSONArray(testCase, jsonParserConfiguration);
+ @Test
+ public void verifyMaxDepthThenDuplicateKey() {
+ JSONParserConfiguration jsonParserConfiguration = new JSONParserConfiguration()
+ .withMaxNestingDepth(42)
+ .withOverwriteDuplicateKey(true);
+
+ assertTrue(jsonParserConfiguration.isOverwriteDuplicateKey());
+ assertEquals(42, jsonParserConfiguration.getMaxNestingDepth());
+ }
+
+ @Test
+ public void givenInvalidInput_testStrictModeTrue_shouldThrowJsonException() {
+ JSONParserConfiguration jsonParserConfiguration = new JSONParserConfiguration()
+ .withStrictMode(true);
+ List strictModeInputTestCases = getNonCompliantJSONArrayList();
+ // this is a lot easier to debug when things stop working
+ for (int i = 0; i < strictModeInputTestCases.size(); ++i) {
+ String testCase = strictModeInputTestCases.get(i);
+ try {
+ JSONArray jsonArray = new JSONArray(testCase, jsonParserConfiguration);
+ String s = jsonArray.toString();
+ String msg = "Expected an exception, but got: " + s + " Noncompliant Array index: " + i;
+ fail(msg);
+ } catch (Exception e) {
+ // its all good
+ }
+ }
+ }
+
+ @Test
+ public void givenInvalidInputObjects_testStrictModeTrue_shouldThrowJsonException() {
+ JSONParserConfiguration jsonParserConfiguration = new JSONParserConfiguration()
+ .withStrictMode(true);
+ List strictModeInputTestCases = getNonCompliantJSONObjectList();
+ // this is a lot easier to debug when things stop working
+ for (int i = 0; i < strictModeInputTestCases.size(); ++i) {
+ String testCase = strictModeInputTestCases.get(i);
+ try {
+ JSONObject jsonObject = new JSONObject(testCase, jsonParserConfiguration);
+ String s = jsonObject.toString();
+ String msg = "Expected an exception, but got: " + s + " Noncompliant Array index: " + i;
+ fail(msg);
+ } catch (Exception e) {
+ // its all good
+ }
+ }
+ }
+ @Test
+ public void givenEmptyArray_testStrictModeTrue_shouldNotThrowJsonException() {
+ JSONParserConfiguration jsonParserConfiguration = new JSONParserConfiguration()
+ .withStrictMode(true);
+ String testCase = "[]";
+ JSONArray jsonArray = new JSONArray(testCase, jsonParserConfiguration);
assertEquals(testCase, jsonArray.toString());
}
@Test
- public void givenValidDoubleArray_testStrictModeTrue_shouldNotThrowJsonException() {
+ public void givenEmptyObject_testStrictModeTrue_shouldNotThrowJsonException() {
JSONParserConfiguration jsonParserConfiguration = new JSONParserConfiguration()
- .withStrictMode(true);
+ .withStrictMode(true);
+ String testCase = "{}";
+ JSONObject jsonObject = new JSONObject(testCase, jsonParserConfiguration);
+ assertEquals(testCase, jsonObject.toString());
+ }
+
+ @Test
+ public void givenValidNestedArray_testStrictModeTrue_shouldNotThrowJsonException() {
+ JSONParserConfiguration jsonParserConfiguration = new JSONParserConfiguration()
+ .withStrictMode(true);
String testCase = "[[\"c\"], [10.2], [true, false, true]]";
@@ -76,48 +156,92 @@ public void givenValidDoubleArray_testStrictModeTrue_shouldNotThrowJsonException
}
@Test
- public void givenValidEmptyArrayInsideArray_testStrictModeTrue_shouldNotThrowJsonException(){
+ public void givenValidNestedObject_testStrictModeTrue_shouldNotThrowJsonException() {
JSONParserConfiguration jsonParserConfiguration = new JSONParserConfiguration()
- .withStrictMode(true);
+ .withStrictMode(true);
- String testCase = "[[]]";
+ String testCase = "{\"a0\":[\"c\"], \"a1\":[10.2], \"a2\":[true, false, true]}";
- JSONArray jsonArray = new JSONArray(testCase, jsonParserConfiguration);
+ JSONObject jsonObject = new JSONObject(testCase, jsonParserConfiguration);
+ JSONArray arrayShouldContainStringAt0 = jsonObject.getJSONArray("a0");
+ JSONArray arrayShouldContainNumberAt0 = jsonObject.getJSONArray("a1");
+ JSONArray arrayShouldContainBooleanAt0 = jsonObject.getJSONArray("a2");
+
+ assertTrue(arrayShouldContainStringAt0.get(0) instanceof String);
+ assertTrue(arrayShouldContainNumberAt0.get(0) instanceof Number);
+ assertTrue(arrayShouldContainBooleanAt0.get(0) instanceof Boolean);
+ }
+ @Test
+ public void givenValidEmptyArrayInsideArray_testStrictModeTrue_shouldNotThrowJsonException(){
+ JSONParserConfiguration jsonParserConfiguration = new JSONParserConfiguration()
+ .withStrictMode(true);
+ String testCase = "[[]]";
+ JSONArray jsonArray = new JSONArray(testCase, jsonParserConfiguration);
assertEquals(testCase, jsonArray.toString());
}
@Test
- public void givenValidEmptyArrayInsideArray_testStrictModeFalse_shouldNotThrowJsonException(){
+ public void givenValidEmptyArrayInsideObject_testStrictModeTrue_shouldNotThrowJsonException(){
JSONParserConfiguration jsonParserConfiguration = new JSONParserConfiguration()
- .withStrictMode(false);
+ .withStrictMode(true);
+ String testCase = "{\"a0\":[]}";
+ JSONObject jsonObject = new JSONObject(testCase, jsonParserConfiguration);
+ assertEquals(testCase, jsonObject.toString());
+ }
+ @Test
+ public void givenValidEmptyArrayInsideArray_testStrictModeFalse_shouldNotThrowJsonException(){
+ JSONParserConfiguration jsonParserConfiguration = new JSONParserConfiguration()
+ .withStrictMode(false);
String testCase = "[[]]";
-
JSONArray jsonArray = new JSONArray(testCase, jsonParserConfiguration);
-
assertEquals(testCase, jsonArray.toString());
}
@Test
- public void givenInvalidString_testStrictModeTrue_shouldThrowJsonException() {
+ public void givenValidEmptyArrayInsideObject_testStrictModeFalse_shouldNotThrowJsonException(){
JSONParserConfiguration jsonParserConfiguration = new JSONParserConfiguration()
- .withStrictMode(true);
+ .withStrictMode(false);
+ String testCase = "{\"a0\":[]}";
+ JSONObject jsonObject = new JSONObject(testCase, jsonParserConfiguration);
+ assertEquals(testCase, jsonObject.toString());
+ }
+ @Test
+ public void givenInvalidStringArray_testStrictModeTrue_shouldThrowJsonException() {
+ JSONParserConfiguration jsonParserConfiguration = new JSONParserConfiguration()
+ .withStrictMode(true);
String testCase = "[badString]";
-
JSONException je = assertThrows(JSONException.class, () -> new JSONArray(testCase, jsonParserConfiguration));
+ assertEquals("Strict mode error: Value 'badString' is not surrounded by quotes at 10 [character 11 line 1]",
+ je.getMessage());
+ }
- assertEquals("Value 'badString' is not surrounded by quotes at 10 [character 11 line 1]", je.getMessage());
+ @Test
+ public void givenInvalidStringObject_testStrictModeTrue_shouldThrowJsonException() {
+ JSONParserConfiguration jsonParserConfiguration = new JSONParserConfiguration()
+ .withStrictMode(true);
+ String testCase = "{\"a0\":badString}";
+ JSONException je = assertThrows(JSONException.class, () -> new JSONObject(testCase, jsonParserConfiguration));
+ assertEquals("Strict mode error: Value 'badString' is not surrounded by quotes at 15 [character 16 line 1]",
+ je.getMessage());
}
@Test
- public void allowNullInStrictMode() {
+ public void allowNullArrayInStrictMode() {
String expected = "[null]";
JSONArray jsonArray = new JSONArray(expected, new JSONParserConfiguration().withStrictMode(true));
assertEquals(expected, jsonArray.toString());
}
+ @Test
+ public void allowNullObjectInStrictMode() {
+ String expected = "{\"a0\":null}";
+ JSONObject jsonObject = new JSONObject(expected, new JSONParserConfiguration().withStrictMode(true));
+ assertEquals(expected, jsonObject.toString());
+ }
+
@Test
public void shouldHandleNumericArray() {
String expected = "[10]";
@@ -125,81 +249,155 @@ public void shouldHandleNumericArray() {
assertEquals(expected, jsonArray.toString());
}
+ @Test
+ public void shouldHandleNumericObject() {
+ String expected = "{\"a0\":10}";
+ JSONObject jsonObject = new JSONObject(expected, new JSONParserConfiguration().withStrictMode(true));
+ assertEquals(expected, jsonObject.toString());
+ }
@Test
public void givenCompliantJSONArrayFile_testStrictModeTrue_shouldNotThrowAnyException() throws IOException {
try (Stream lines = Files.lines(Paths.get("src/test/resources/compliantJsonArray.json"))) {
String compliantJsonArrayAsString = lines.collect(Collectors.joining());
JSONParserConfiguration jsonParserConfiguration = new JSONParserConfiguration()
- .withStrictMode(true);
-
+ .withStrictMode(true);
new JSONArray(compliantJsonArrayAsString, jsonParserConfiguration);
}
+ }
+
+ @Test
+ public void givenCompliantJSONObjectFile_testStrictModeTrue_shouldNotThrowAnyException() throws IOException {
+ try (Stream lines = Files.lines(Paths.get("src/test/resources/compliantJsonObject.json"))) {
+ String compliantJsonObjectAsString = lines.collect(Collectors.joining());
+ JSONParserConfiguration jsonParserConfiguration = new JSONParserConfiguration()
+ .withStrictMode(true);
+ new JSONObject(compliantJsonObjectAsString, jsonParserConfiguration);
+ }
}
@Test
public void givenInvalidInputArrays_testStrictModeFalse_shouldNotThrowAnyException() {
JSONParserConfiguration jsonParserConfiguration = new JSONParserConfiguration()
- .withStrictMode(false);
-
- List strictModeInputTestCases = getNonCompliantJSONList();
+ .withStrictMode(false);
+
+ List strictModeInputTestCases = getNonCompliantJSONArrayList();
+
+ // this is a lot easier to debug when things stop working
+ for (int i = 0; i < strictModeInputTestCases.size(); ++i) {
+ String testCase = strictModeInputTestCases.get(i);
+ try {
+ JSONArray jsonArray = new JSONArray(testCase, jsonParserConfiguration);
+ } catch (Exception e) {
+ System.out.println("Unexpected exception: " + e.getMessage() + " Noncompliant Array index: " + i);
+ fail(String.format("Noncompliant array index: %d", i));
+ }
+ }
+ }
- strictModeInputTestCases.forEach(testCase -> new JSONArray(testCase, jsonParserConfiguration));
+ @Test
+ public void givenInvalidInputObjects_testStrictModeFalse_shouldNotThrowAnyException() {
+ JSONParserConfiguration jsonParserConfiguration = new JSONParserConfiguration()
+ .withStrictMode(false);
+
+ List strictModeInputTestCases = getNonCompliantJSONObjectList();
+
+ // this is a lot easier to debug when things stop working
+ for (int i = 0; i < strictModeInputTestCases.size(); ++i) {
+ String testCase = strictModeInputTestCases.get(i);
+ try {
+ JSONObject jsonObject = new JSONObject(testCase, jsonParserConfiguration);
+ } catch (Exception e) {
+ System.out.println("Unexpected exception: " + e.getMessage() + " Noncompliant Array index: " + i);
+ fail(String.format("Noncompliant array index: %d", i));
+ }
+ }
}
@Test
public void givenInvalidInputArray_testStrictModeTrue_shouldThrowInvalidCharacterErrorMessage() {
JSONParserConfiguration jsonParserConfiguration = new JSONParserConfiguration()
- .withStrictMode(true);
-
+ .withStrictMode(true);
String testCase = "[1,2];[3,4]";
-
JSONException je = assertThrows("expected non-compliant array but got instead: " + testCase,
- JSONException.class, () -> new JSONArray(testCase, jsonParserConfiguration));
+ JSONException.class, () -> new JSONArray(testCase, jsonParserConfiguration));
+ assertEquals("Strict mode error: Unparsed characters found at end of input text at 6 [character 7 line 1]",
+ je.getMessage());
+ }
- assertEquals("invalid character ';' found after end of array at 6 [character 7 line 1]", je.getMessage());
+ @Test
+ public void givenInvalidInputObject_testStrictModeTrue_shouldThrowInvalidCharacterErrorMessage() {
+ JSONParserConfiguration jsonParserConfiguration = new JSONParserConfiguration()
+ .withStrictMode(true);
+ String testCase = "{\"a0\":[1,2];\"a1\":[3,4]}";
+ JSONException je = assertThrows("expected non-compliant array but got instead: " + testCase,
+ JSONException.class, () -> new JSONObject(testCase, jsonParserConfiguration));
+ assertEquals("Strict mode error: Invalid character ';' found at 12 [character 13 line 1]", je.getMessage());
}
@Test
public void givenInvalidInputArrayWithNumericStrings_testStrictModeTrue_shouldThrowInvalidCharacterErrorMessage() {
JSONParserConfiguration jsonParserConfiguration = new JSONParserConfiguration()
- .withStrictMode(true);
-
+ .withStrictMode(true);
String testCase = "[\"1\",\"2\"];[3,4]";
-
JSONException je = assertThrows("expected non-compliant array but got instead: " + testCase,
- JSONException.class, () -> new JSONArray(testCase, jsonParserConfiguration));
+ JSONException.class, () -> new JSONArray(testCase, jsonParserConfiguration));
+ assertEquals("Strict mode error: Unparsed characters found at end of input text at 10 [character 11 line 1]",
+ je.getMessage());
+ }
- assertEquals("invalid character ';' found after end of array at 10 [character 11 line 1]", je.getMessage());
+ @Test
+ public void givenInvalidInputObjectWithNumericStrings_testStrictModeTrue_shouldThrowInvalidCharacterErrorMessage() {
+ JSONParserConfiguration jsonParserConfiguration = new JSONParserConfiguration()
+ .withStrictMode(true);
+ String testCase = "{\"a0\":[\"1\",\"2\"];\"a1\":[3,4]}";
+ JSONException je = assertThrows("expected non-compliant array but got instead: " + testCase,
+ JSONException.class, () -> new JSONObject(testCase, jsonParserConfiguration));
+ assertEquals("Strict mode error: Invalid character ';' found at 16 [character 17 line 1]", je.getMessage());
}
@Test
public void givenInvalidInputArray_testStrictModeTrue_shouldThrowValueNotSurroundedByQuotesErrorMessage() {
JSONParserConfiguration jsonParserConfiguration = new JSONParserConfiguration()
- .withStrictMode(true);
-
+ .withStrictMode(true);
String testCase = "[{\"test\": implied}]";
-
JSONException je = assertThrows("expected non-compliant array but got instead: " + testCase,
- JSONException.class, () -> new JSONArray(testCase, jsonParserConfiguration));
+ JSONException.class, () -> new JSONArray(testCase, jsonParserConfiguration));
+ assertEquals("Strict mode error: Value 'implied' is not surrounded by quotes at 17 [character 18 line 1]",
+ je.getMessage());
+ }
- assertEquals("Value 'implied' is not surrounded by quotes at 17 [character 18 line 1]", je.getMessage());
+ @Test
+ public void givenInvalidInputObject_testStrictModeTrue_shouldThrowValueNotSurroundedByQuotesErrorMessage() {
+ JSONParserConfiguration jsonParserConfiguration = new JSONParserConfiguration()
+ .withStrictMode(true);
+ String testCase = "{\"a0\":{\"test\": implied}]}";
+ JSONException je = assertThrows("expected non-compliant array but got instead: " + testCase,
+ JSONException.class, () -> new JSONObject(testCase, jsonParserConfiguration));
+ assertEquals("Strict mode error: Value 'implied' is not surrounded by quotes at 22 [character 23 line 1]",
+ je.getMessage());
}
@Test
public void givenInvalidInputArray_testStrictModeFalse_shouldNotThrowAnyException() {
JSONParserConfiguration jsonParserConfiguration = new JSONParserConfiguration()
- .withStrictMode(false);
-
+ .withStrictMode(false);
String testCase = "[{\"test\": implied}]";
-
new JSONArray(testCase, jsonParserConfiguration);
}
@Test
- public void givenNonCompliantQuotes_testStrictModeTrue_shouldThrowJsonExceptionWithConcreteErrorDescription() {
+ public void givenInvalidInputObject_testStrictModeFalse_shouldNotThrowAnyException() {
JSONParserConfiguration jsonParserConfiguration = new JSONParserConfiguration()
- .withStrictMode(true);
+ .withStrictMode(false);
+ String testCase = "{\"a0\":{\"test\": implied}}";
+ new JSONObject(testCase, jsonParserConfiguration);
+ }
+
+ @Test
+ public void givenNonCompliantQuotesArray_testStrictModeTrue_shouldThrowJsonExceptionWithConcreteErrorDescription() {
+ JSONParserConfiguration jsonParserConfiguration = new JSONParserConfiguration()
+ .withStrictMode(true);
String testCaseOne = "[\"abc', \"test\"]";
String testCaseTwo = "['abc\", \"test\"]";
@@ -207,76 +405,187 @@ public void givenNonCompliantQuotes_testStrictModeTrue_shouldThrowJsonExceptionW
String testCaseFour = "[{'testField': \"testValue\"}]";
JSONException jeOne = assertThrows(JSONException.class,
- () -> new JSONArray(testCaseOne, jsonParserConfiguration));
+ () -> new JSONArray(testCaseOne, jsonParserConfiguration));
JSONException jeTwo = assertThrows(JSONException.class,
- () -> new JSONArray(testCaseTwo, jsonParserConfiguration));
+ () -> new JSONArray(testCaseTwo, jsonParserConfiguration));
JSONException jeThree = assertThrows(JSONException.class,
- () -> new JSONArray(testCaseThree, jsonParserConfiguration));
+ () -> new JSONArray(testCaseThree, jsonParserConfiguration));
JSONException jeFour = assertThrows(JSONException.class,
- () -> new JSONArray(testCaseFour, jsonParserConfiguration));
+ () -> new JSONArray(testCaseFour, jsonParserConfiguration));
assertEquals(
- "Value 'test' is not surrounded by quotes at 13 [character 14 line 1]",
- jeOne.getMessage());
+ "Expected a ',' or ']' at 10 [character 11 line 1]",
+ jeOne.getMessage());
assertEquals(
- "Single quote wrap not allowed in strict mode at 2 [character 3 line 1]",
- jeTwo.getMessage());
+ "Strict mode error: Single quoted strings are not allowed at 2 [character 3 line 1]",
+ jeTwo.getMessage());
assertEquals(
- "Single quote wrap not allowed in strict mode at 2 [character 3 line 1]",
- jeThree.getMessage());
+ "Strict mode error: Single quoted strings are not allowed at 2 [character 3 line 1]",
+ jeThree.getMessage());
assertEquals(
- "Single quote wrap not allowed in strict mode at 3 [character 4 line 1]",
- jeFour.getMessage());
+ "Strict mode error: Single quoted strings are not allowed at 3 [character 4 line 1]",
+ jeFour.getMessage());
}
@Test
- public void givenUnbalancedQuotes_testStrictModeFalse_shouldThrowJsonException() {
+ public void givenNonCompliantQuotesObject_testStrictModeTrue_shouldThrowJsonExceptionWithConcreteErrorDescription() {
JSONParserConfiguration jsonParserConfiguration = new JSONParserConfiguration()
- .withStrictMode(false);
+ .withStrictMode(true);
+
+ String testCaseOne = "{\"abc': \"test\"}";
+ String testCaseTwo = "{'abc\": \"test\"}";
+ String testCaseThree = "{\"a\":'abc'}";
+ String testCaseFour = "{'testField': \"testValue\"}";
+
+ JSONException jeOne = assertThrows(JSONException.class,
+ () -> new JSONObject(testCaseOne, jsonParserConfiguration));
+ JSONException jeTwo = assertThrows(JSONException.class,
+ () -> new JSONObject(testCaseTwo, jsonParserConfiguration));
+ JSONException jeThree = assertThrows(JSONException.class,
+ () -> new JSONObject(testCaseThree, jsonParserConfiguration));
+ JSONException jeFour = assertThrows(JSONException.class,
+ () -> new JSONObject(testCaseFour, jsonParserConfiguration));
+
+ assertEquals(
+ "Expected a ':' after a key at 10 [character 11 line 1]",
+ jeOne.getMessage());
+ assertEquals(
+ "Strict mode error: Single quoted strings are not allowed at 2 [character 3 line 1]",
+ jeTwo.getMessage());
+ assertEquals(
+ "Strict mode error: Single quoted strings are not allowed at 6 [character 7 line 1]",
+ jeThree.getMessage());
+ assertEquals(
+ "Strict mode error: Single quoted strings are not allowed at 2 [character 3 line 1]",
+ jeFour.getMessage());
+ }
+
+ @Test
+ public void givenUnbalancedQuotesArray_testStrictModeFalse_shouldThrowJsonException() {
+ JSONParserConfiguration jsonParserConfiguration = new JSONParserConfiguration()
+ .withStrictMode(false);
String testCaseOne = "[\"abc', \"test\"]";
String testCaseTwo = "['abc\", \"test\"]";
JSONException jeOne = assertThrows(JSONException.class,
- () -> new JSONArray(testCaseOne, jsonParserConfiguration));
+ () -> new JSONArray(testCaseOne, jsonParserConfiguration));
JSONException jeTwo = assertThrows(JSONException.class,
- () -> new JSONArray(testCaseTwo, jsonParserConfiguration));
+ () -> new JSONArray(testCaseTwo, jsonParserConfiguration));
- assertEquals("Unterminated string. Character with int code 0 is not allowed within a quoted string. at 15 [character 16 line 1]", jeOne.getMessage());
+ assertEquals("Expected a ',' or ']' at 10 [character 11 line 1]", jeOne.getMessage());
assertEquals("Unterminated string. Character with int code 0 is not allowed within a quoted string. at 15 [character 16 line 1]", jeTwo.getMessage());
}
+ @Test
+ public void givenUnbalancedQuotesObject_testStrictModeFalse_shouldThrowJsonException() {
+ JSONParserConfiguration jsonParserConfiguration = new JSONParserConfiguration()
+ .withStrictMode(false);
+
+ String testCaseOne = "{\"abc': \"test\"}";
+ String testCaseTwo = "{'abc\": \"test\"}";
+
+ JSONException jeOne = assertThrows(JSONException.class,
+ () -> new JSONObject(testCaseOne, jsonParserConfiguration));
+ JSONException jeTwo = assertThrows(JSONException.class,
+ () -> new JSONObject(testCaseTwo, jsonParserConfiguration));
+
+ assertEquals("Expected a ':' after a key at 10 [character 11 line 1]", jeOne.getMessage());
+ assertEquals("Unterminated string. Character with int code 0 is not allowed within a quoted string. at 15 [character 16 line 1]", jeTwo.getMessage());
+ }
@Test
public void givenInvalidInputArray_testStrictModeTrue_shouldThrowKeyNotSurroundedByQuotesErrorMessage() {
JSONParserConfiguration jsonParserConfiguration = new JSONParserConfiguration()
- .withStrictMode(true);
+ .withStrictMode(true);
String testCase = "[{test: implied}]";
JSONException je = assertThrows("expected non-compliant array but got instead: " + testCase,
- JSONException.class, () -> new JSONArray(testCase, jsonParserConfiguration));
+ JSONException.class, () -> new JSONArray(testCase, jsonParserConfiguration));
- assertEquals("Value 'test' is not surrounded by quotes at 6 [character 7 line 1]", je.getMessage());
+ assertEquals("Strict mode error: Value 'test' is not surrounded by quotes at 6 [character 7 line 1]",
+ je.getMessage());
}
@Test
- public void verifyDuplicateKeyThenMaxDepth() {
+ public void givenInvalidInputObject_testStrictModeTrue_shouldThrowKeyNotSurroundedByQuotesErrorMessage() {
JSONParserConfiguration jsonParserConfiguration = new JSONParserConfiguration()
- .withOverwriteDuplicateKey(true)
- .withMaxNestingDepth(42);
+ .withStrictMode(true);
- assertEquals(42, jsonParserConfiguration.getMaxNestingDepth());
- assertTrue(jsonParserConfiguration.isOverwriteDuplicateKey());
+ String testCase = "{test: implied}";
+ JSONException je = assertThrows("expected non-compliant json but got instead: " + testCase,
+ JSONException.class, () -> new JSONObject(testCase, jsonParserConfiguration));
+
+ assertEquals("Strict mode error: Value 'test' is not surrounded by quotes at 5 [character 6 line 1]",
+ je.getMessage());
}
@Test
- public void verifyMaxDepthThenDuplicateKey() {
- JSONParserConfiguration jsonParserConfiguration = new JSONParserConfiguration()
- .withMaxNestingDepth(42)
- .withOverwriteDuplicateKey(true);
+ public void givenInvalidInputObject_testStrictModeTrue_JSONObjectUsingJSONTokener_shouldThrowJSONException() {
+ JSONException exception = assertThrows(JSONException.class, () -> {
+ new JSONObject(new JSONTokener("{\"key\":\"value\"} invalid trailing text"), new JSONParserConfiguration().withStrictMode(true));
+ });
- assertTrue(jsonParserConfiguration.isOverwriteDuplicateKey());
- assertEquals(42, jsonParserConfiguration.getMaxNestingDepth());
+ assertEquals("Strict mode error: Unparsed characters found at end of input text at 17 [character 18 line 1]", exception.getMessage());
+ }
+
+ @Test
+ public void givenInvalidInputObject_testStrictModeTrue_JSONObjectUsingString_shouldThrowJSONException() {
+ JSONException exception = assertThrows(JSONException.class, () -> {
+ new JSONObject("{\"key\":\"value\"} invalid trailing text", new JSONParserConfiguration().withStrictMode(true));
+ });
+ assertEquals("Strict mode error: Unparsed characters found at end of input text at 17 [character 18 line 1]", exception.getMessage());
+ }
+
+ @Test
+ public void givenInvalidInputObject_testStrictModeTrue_JSONArrayUsingJSONTokener_shouldThrowJSONException() {
+ JSONException exception = assertThrows(JSONException.class, () -> {
+ new JSONArray(new JSONTokener("[\"value\"] invalid trailing text"), new JSONParserConfiguration().withStrictMode(true));
+ });
+
+ assertEquals("Strict mode error: Unparsed characters found at end of input text at 11 [character 12 line 1]", exception.getMessage());
+ }
+
+ @Test
+ public void givenInvalidInputObject_testStrictModeTrue_JSONArrayUsingString_shouldThrowJSONException() {
+ JSONException exception = assertThrows(JSONException.class, () -> {
+ new JSONArray("[\"value\"] invalid trailing text", new JSONParserConfiguration().withStrictMode(true));
+ });
+ assertEquals("Strict mode error: Unparsed characters found at end of input text at 11 [character 12 line 1]", exception.getMessage());
+ }
+
+ /**
+ * This method contains short but focused use-case samples and is exclusively used to test strictMode unit tests in
+ * this class.
+ *
+ * @return List with JSON strings.
+ */
+ private List getNonCompliantJSONArrayList() {
+ return Arrays.asList(
+ "[1],",
+ "[1,]",
+ "[,]",
+ "[,,]",
+ "[[1],\"sa\",[2]]a",
+ "[1],\"dsa\": \"test\"",
+ "[[a]]",
+ "[]asdf",
+ "[]]",
+ "[]}",
+ "[][",
+ "[]{",
+ "[],",
+ "[]:",
+ "[],[",
+ "[],{",
+ "[1,2];[3,4]",
+ "[test]",
+ "[{'testSingleQuote': 'testSingleQuote'}]",
+ "[1, 2,3]:[4,5]",
+ "[{test: implied}]",
+ "[{\"test\": implied}]",
+ "[{\"number\":\"7990154836330\",\"color\":'c'},{\"number\":8784148854580,\"color\":RosyBrown},{\"number\":\"5875770107113\",\"color\":\"DarkSeaGreen\"}]",
+ "[{test: \"implied\"}]");
}
/**
@@ -285,29 +594,31 @@ public void verifyMaxDepthThenDuplicateKey() {
*
* @return List with JSON strings.
*/
- private List getNonCompliantJSONList() {
+ private List getNonCompliantJSONObjectList() {
return Arrays.asList(
- "[1],",
- "[1,]",
- "[[1]\"sa\",[2]]a",
- "[1],\"dsa\": \"test\"",
- "[[a]]",
- "[]asdf",
- "[]]",
- "[]}",
- "[][",
- "[]{",
- "[],",
- "[]:",
- "[],[",
- "[],{",
- "[1,2];[3,4]",
- "[test]",
- "[{'testSingleQuote': 'testSingleQuote'}]",
- "[1, 2,3]:[4,5]",
- "[{test: implied}]",
- "[{\"test\": implied}]",
- "[{\"number\":\"7990154836330\",\"color\":'c'},{\"number\":8784148854580,\"color\":RosyBrown},{\"number\":\"5875770107113\",\"color\":\"DarkSeaGreen\"}]",
- "[{test: \"implied\"}]");
+ "{\"a\":1},",
+ "{\"a\":1,}",
+ "{\"a0\":[1],\"a1\":\"sa\",\"a2\":[2]}a",
+ "{\"a\":1},\"dsa\": \"test\"",
+ "{\"a\":[a]}",
+ "{}asdf",
+ "{}}",
+ "{}]",
+ "{}{",
+ "{}[",
+ "{},",
+ "{}:",
+ "{},{",
+ "{},[",
+ "{\"a0\":[1,2];\"a1\":[3,4]}",
+ "{\"a\":test}",
+ "{a:{'testSingleQuote': 'testSingleQuote'}}",
+ "{\"a0\":1, \"a1\":2,\"a2\":3}:{\"a3\":4,\"a4\":5}",
+ "{\"a\":{test: implied}}",
+ "{a:{\"test\": implied}}",
+ "{a:[{\"number\":\"7990154836330\",\"color\":'c'},{\"number\":8784148854580,\"color\":RosyBrown},{\"number\":\"5875770107113\",\"color\":\"DarkSeaGreen\"}]}",
+ "{a:{test: \"implied\"}}"
+ );
}
+
}
diff --git a/src/test/java/org/json/junit/JSONPointerTest.java b/src/test/java/org/json/junit/JSONPointerTest.java
index 45c7dbd3d..a420b297f 100644
--- a/src/test/java/org/json/junit/JSONPointerTest.java
+++ b/src/test/java/org/json/junit/JSONPointerTest.java
@@ -384,8 +384,7 @@ public void queryFromJSONObjectUsingPointer0() {
String str = "{"+
"\"string\\\\\\\\Key\":\"hello world!\","+
- "\"\\\\\":\"slash test\"," +
- "}"+
+ "\"\\\\\":\"slash test\"" +
"}";
JSONObject jsonObject = new JSONObject(str);
//Summary of issue: When a KEY in the jsonObject is "\\\\" --> it's held
diff --git a/src/test/java/org/json/junit/JSONTokenerTest.java b/src/test/java/org/json/junit/JSONTokenerTest.java
index 59ca6d8f6..b0b45cb7c 100644
--- a/src/test/java/org/json/junit/JSONTokenerTest.java
+++ b/src/test/java/org/json/junit/JSONTokenerTest.java
@@ -16,10 +16,7 @@
import java.io.Reader;
import java.io.StringReader;
-import org.json.JSONArray;
-import org.json.JSONException;
-import org.json.JSONObject;
-import org.json.JSONTokener;
+import org.json.*;
import org.junit.Test;
/**
@@ -98,7 +95,17 @@ public void testValid() {
checkValid(" [] ",JSONArray.class);
checkValid("[1,2]",JSONArray.class);
checkValid("\n\n[1,2]\n\n",JSONArray.class);
- checkValid("1 2", String.class);
+
+ // Test should fail if default strictMode is true, pass if false
+ JSONParserConfiguration jsonParserConfiguration = new JSONParserConfiguration();
+ if (jsonParserConfiguration.isStrictMode()) {
+ try {
+ checkValid("1 2", String.class);
+ assertEquals("Expected to throw exception due to invalid string", true, false);
+ } catch (JSONException e) { }
+ } else {
+ checkValid("1 2", String.class);
+ }
}
@Test
@@ -325,4 +332,42 @@ public void testAutoClose(){
assertEquals("Stream closed", exception.getMessage());
}
}
+
+ @Test
+ public void testInvalidInput_JSONObject_withoutStrictModel_shouldParseInput() {
+ String input = "{\"invalidInput\": [],}";
+ JSONTokener tokener = new JSONTokener(input);
+
+ // Test should fail if default strictMode is true, pass if false
+ JSONParserConfiguration jsonParserConfiguration = new JSONParserConfiguration();
+ if (jsonParserConfiguration.isStrictMode()) {
+ try {
+ Object value = tokener.nextValue();
+ assertEquals(new JSONObject(input).toString(), value.toString());
+ assertEquals("Expected to throw exception due to invalid string", true, false);
+ } catch (JSONException e) { }
+ } else {
+ Object value = tokener.nextValue();
+ assertEquals(new JSONObject(input).toString(), value.toString());
+ }
+ }
+
+ @Test
+ public void testInvalidInput_JSONArray_withoutStrictModel_shouldParseInput() {
+ String input = "[\"invalidInput\",]";
+ JSONTokener tokener = new JSONTokener(input);
+
+ // Test should fail if default strictMode is true, pass if false
+ JSONParserConfiguration jsonParserConfiguration = new JSONParserConfiguration();
+ if (jsonParserConfiguration.isStrictMode()) {
+ try {
+ Object value = tokener.nextValue();
+ assertEquals(new JSONArray(input).toString(), value.toString());
+ assertEquals("Expected to throw exception due to invalid string", true, false);
+ } catch (JSONException e) { }
+ } else {
+ Object value = tokener.nextValue();
+ assertEquals(new JSONArray(input).toString(), value.toString());
+ }
+ }
}
diff --git a/src/test/java/org/json/junit/XMLConfigurationTest.java b/src/test/java/org/json/junit/XMLConfigurationTest.java
index 92a109ad9..ca1980c8a 100755
--- a/src/test/java/org/json/junit/XMLConfigurationTest.java
+++ b/src/test/java/org/json/junit/XMLConfigurationTest.java
@@ -268,9 +268,6 @@ public void shouldHandleSimpleXML() {
" \n"+
"";
- // TODO: This test failed in strictMode due to -23x.45 not being surrounded by quotes
- // It should probably be split into two tests, one of which does not run in strictMode.
- // TBD.
String expectedStr =
"{\"addresses\":{\"address\":{\"street\":\"[CDATA[Baker street 5]\","+
"\"name\":\"Joe Tester\",\"NothingHere\":\"\",\"TrueValue\":true,\n"+
@@ -577,15 +574,18 @@ public void shouldKeepConfigurationIntactAndUpdateCloseEmptyTagChoice()
XMLParserConfiguration keepStringsAndCloseEmptyTag = keepStrings.withCloseEmptyTag(true);
XMLParserConfiguration keepDigits = keepStringsAndCloseEmptyTag.withKeepStrings(false);
XMLParserConfiguration keepDigitsAndNoCloseEmptyTag = keepDigits.withCloseEmptyTag(false);
- assertTrue(keepStrings.isKeepStrings());
+ assertTrue(keepStrings.isKeepNumberAsString());
+ assertTrue(keepStrings.isKeepBooleanAsString());
assertFalse(keepStrings.isCloseEmptyTag());
- assertTrue(keepStringsAndCloseEmptyTag.isKeepStrings());
+ assertTrue(keepStringsAndCloseEmptyTag.isKeepNumberAsString());
+ assertTrue(keepStringsAndCloseEmptyTag.isKeepBooleanAsString());
assertTrue(keepStringsAndCloseEmptyTag.isCloseEmptyTag());
- assertFalse(keepDigits.isKeepStrings());
+ assertFalse(keepDigits.isKeepNumberAsString());
+ assertFalse(keepDigits.isKeepBooleanAsString());
assertTrue(keepDigits.isCloseEmptyTag());
- assertFalse(keepDigitsAndNoCloseEmptyTag.isKeepStrings());
+ assertFalse(keepDigitsAndNoCloseEmptyTag.isKeepNumberAsString());
+ assertFalse(keepDigitsAndNoCloseEmptyTag.isKeepBooleanAsString());
assertFalse(keepDigitsAndNoCloseEmptyTag.isCloseEmptyTag());
-
}
/**
@@ -770,6 +770,67 @@ public void testToJSONArray_jsonOutput() {
Util.compareActualVsExpectedJsonObjects(actualJsonOutput,expected);
}
+ /**
+ * JSON string lost leading zero and converted "True" to true.
+ */
+ @Test
+ public void testToJSONArray_jsonOutput_withKeepNumberAsString() {
+ final String originalXml = "01 1 00 0 null True ";
+ final JSONObject expected = new JSONObject("{\"root\":{\"item\":{\"id\":\"01\"},\"id\":[\"01\",\"1\",\"00\",\"0\",null],\"title\":true}}");
+ final JSONObject actualJsonOutput = XML.toJSONObject(originalXml,
+ new XMLParserConfiguration().withKeepNumberAsString(true));
+ Util.compareActualVsExpectedJsonObjects(actualJsonOutput,expected);
+ }
+
+ /**
+ * JSON string lost leading zero and converted "True" to true.
+ */
+ @Test
+ public void testToJSONArray_jsonOutput_withKeepBooleanAsString() {
+ final String originalXml = "01 1 00 0 null True ";
+ final JSONObject expected = new JSONObject("{\"root\":{\"item\":{\"id\":\"01\"},\"id\":[\"01\",1,\"00\",0,null],\"title\":\"True\"}}");
+ final JSONObject actualJsonOutput = XML.toJSONObject(originalXml,
+ new XMLParserConfiguration().withKeepBooleanAsString(true));
+ Util.compareActualVsExpectedJsonObjects(actualJsonOutput,expected);
+ }
+
+ /**
+ * null is "null" when keepStrings == true
+ */
+ @Test
+ public void testToJSONArray_jsonOutput_null_withKeepString() {
+ final String originalXml = "01 1 00 0 null ";
+ final JSONObject expected = new JSONObject("{\"root\":{\"item\":{\"id\":\"01\"},\"id\":[\"01\",\"1\",\"00\",\"0\"],\"title\":\"null\"}}");
+ final JSONObject actualJsonOutput = XML.toJSONObject(originalXml,
+ new XMLParserConfiguration().withKeepStrings(true));
+ Util.compareActualVsExpectedJsonObjects(actualJsonOutput,expected);
+ }
+
+ /**
+ * Test keepStrings behavior when setting keepBooleanAsString, keepNumberAsString
+ */
+ @Test
+ public void test_keepStringBehavior() {
+ XMLParserConfiguration xpc = new XMLParserConfiguration().withKeepStrings(true);
+ assertEquals(xpc.isKeepStrings(), true);
+
+ xpc = xpc.withKeepBooleanAsString(true);
+ xpc = xpc.withKeepNumberAsString(false);
+ assertEquals(xpc.isKeepStrings(), false);
+
+ xpc = xpc.withKeepBooleanAsString(false);
+ xpc = xpc.withKeepNumberAsString(true);
+ assertEquals(xpc.isKeepStrings(), false);
+
+ xpc = xpc.withKeepBooleanAsString(true);
+ xpc = xpc.withKeepNumberAsString(true);
+ assertEquals(xpc.isKeepStrings(), true);
+
+ xpc = xpc.withKeepBooleanAsString(false);
+ xpc = xpc.withKeepNumberAsString(false);
+ assertEquals(xpc.isKeepStrings(), false);
+ }
+
/**
* JSON string cannot be reverted to original xml.
*/
diff --git a/src/test/java/org/json/junit/XMLTest.java b/src/test/java/org/json/junit/XMLTest.java
index be478643c..2fa5daeea 100644
--- a/src/test/java/org/json/junit/XMLTest.java
+++ b/src/test/java/org/json/junit/XMLTest.java
@@ -265,8 +265,6 @@ public void shouldHandleSimpleXML() {
" \n"+
"";
- // TODO: fails in strict mode because -23x.45 was not surrounded by quotes.
- // Should be split into a strictMode test, and a similar non-strictMode test
String expectedStr =
"{\"addresses\":{\"address\":{\"street\":\"[CDATA[Baker street 5]\","+
"\"name\":\"Joe Tester\",\"NothingHere\":\"\",\"TrueValue\":true,\n"+
diff --git a/src/test/java/org/json/junit/XMLTokenerTest.java b/src/test/java/org/json/junit/XMLTokenerTest.java
new file mode 100644
index 000000000..ca2f2075e
--- /dev/null
+++ b/src/test/java/org/json/junit/XMLTokenerTest.java
@@ -0,0 +1,81 @@
+package org.json.junit;
+
+import org.json.XMLTokener;
+import org.junit.Test;
+
+import java.io.StringReader;
+
+import static org.junit.Assert.*;
+
+/**
+ * Tests for JSON-Java XMLTokener.java
+ */
+public class XMLTokenerTest {
+
+ /**
+ * Tests that nextCDATA() correctly extracts content from within a CDATA section.
+ */
+ @Test
+ public void testNextCDATA() {
+ String xml = "This is content ]]> after";
+ XMLTokener tokener = new XMLTokener(new StringReader(xml));
+ tokener.skipPast(" content ", cdata);
+ }
+
+ /**
+ * Tests that nextContent() returns plain text content before a tag.
+ */
+ @Test
+ public void testNextContentWithText() {
+ String xml = "Some content";
+ XMLTokener tokener = new XMLTokener(xml);
+ Object content = tokener.nextContent();
+ assertEquals("Some content", content);
+ }
+
+ /**
+ * Tests that nextContent() returns '<' character when starting with a tag.
+ */
+ @Test
+ public void testNextContentWithTag() {
+ String xml = "";
+ XMLTokener tokener = new XMLTokener(xml);
+ Object content = tokener.nextContent();
+ assertEquals('<', content);
+ }
+
+ /**
+ * Tests that nextEntity() resolves a known entity like & correctly.
+ */
+ @Test
+ public void testNextEntityKnown() {
+ XMLTokener tokener = new XMLTokener("amp;");
+ Object result = tokener.nextEntity('&');
+ assertEquals("&", result);
+ }
+
+ /**
+ * Tests that nextEntity() preserves unknown entities by returning them unchanged.
+ */
+ @Test
+ public void testNextEntityUnknown() {
+ XMLTokener tokener = new XMLTokener("unknown;");
+ tokener.next(); // skip 'u'
+ Object result = tokener.nextEntity('&');
+ assertEquals("&nknown;", result); // malformed start to simulate unknown
+ }
+
+ /**
+ * Tests skipPast() to ensure the cursor moves past the specified string.
+ */
+ @Test
+ public void testSkipPast() {
+ String xml = "Ignore this... endHere more text";
+ XMLTokener tokener = new XMLTokener(xml);
+ tokener.skipPast("endHere");
+ assertEquals(' ', tokener.next()); // should be the space after "endHere"
+ }
+
+}
\ No newline at end of file
diff --git a/src/test/java/org/json/junit/data/CustomClass.java b/src/test/java/org/json/junit/data/CustomClass.java
new file mode 100644
index 000000000..9ae405597
--- /dev/null
+++ b/src/test/java/org/json/junit/data/CustomClass.java
@@ -0,0 +1,23 @@
+package org.json.junit.data;
+
+public 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));
+ }
+}
+
diff --git a/src/test/java/org/json/junit/data/CustomClassA.java b/src/test/java/org/json/junit/data/CustomClassA.java
new file mode 100644
index 000000000..08a99d333
--- /dev/null
+++ b/src/test/java/org/json/junit/data/CustomClassA.java
@@ -0,0 +1,19 @@
+package org.json.junit.data;
+
+import java.math.BigInteger;
+
+public class CustomClassA {
+ public BigInteger largeInt;
+
+ public CustomClassA() {}
+ public CustomClassA(BigInteger largeInt) {
+ this.largeInt = largeInt;
+ }
+
+ @Override
+ public boolean equals(Object o) {
+ CustomClassA classA = (CustomClassA) o;
+ return this.largeInt.equals(classA.largeInt);
+ }
+}
+
diff --git a/src/test/java/org/json/junit/data/CustomClassB.java b/src/test/java/org/json/junit/data/CustomClassB.java
new file mode 100644
index 000000000..688997ec4
--- /dev/null
+++ b/src/test/java/org/json/junit/data/CustomClassB.java
@@ -0,0 +1,20 @@
+package org.json.junit.data;
+
+public 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);
+ }
+}
+
diff --git a/src/test/java/org/json/junit/data/CustomClassC.java b/src/test/java/org/json/junit/data/CustomClassC.java
new file mode 100644
index 000000000..9d20aa392
--- /dev/null
+++ b/src/test/java/org/json/junit/data/CustomClassC.java
@@ -0,0 +1,34 @@
+package org.json.junit.data;
+
+import org.json.JSONObject;
+
+public 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);
+ }
+}
+
diff --git a/src/test/java/org/json/junit/data/CustomClassD.java b/src/test/java/org/json/junit/data/CustomClassD.java
new file mode 100644
index 000000000..4a858058c
--- /dev/null
+++ b/src/test/java/org/json/junit/data/CustomClassD.java
@@ -0,0 +1,19 @@
+package org.json.junit.data;
+
+import java.util.List;
+
+public class CustomClassD {
+ public List stringList;
+
+ public CustomClassD() {}
+ public CustomClassD(List stringList) {
+ this.stringList = stringList;
+ }
+
+ @Override
+ public boolean equals(Object o) {
+ CustomClassD classD = (CustomClassD) o;
+ return this.stringList.equals(classD.stringList);
+ }
+}
+
diff --git a/src/test/java/org/json/junit/data/CustomClassE.java b/src/test/java/org/json/junit/data/CustomClassE.java
new file mode 100644
index 000000000..807dc5540
--- /dev/null
+++ b/src/test/java/org/json/junit/data/CustomClassE.java
@@ -0,0 +1,18 @@
+package org.json.junit.data;
+
+import java.util.List;
+
+public class CustomClassE {
+ public List listClassC;
+
+ public CustomClassE() {}
+ public CustomClassE(List listClassC) {
+ this.listClassC = listClassC;
+ }
+
+ @Override
+ public boolean equals(Object o) {
+ CustomClassE classE = (CustomClassE) o;
+ return this.listClassC.equals(classE.listClassC);
+ }
+}
diff --git a/src/test/java/org/json/junit/data/CustomClassF.java b/src/test/java/org/json/junit/data/CustomClassF.java
new file mode 100644
index 000000000..d85861036
--- /dev/null
+++ b/src/test/java/org/json/junit/data/CustomClassF.java
@@ -0,0 +1,19 @@
+package org.json.junit.data;
+
+import java.util.List;
+
+public class CustomClassF {
+ public List> listOfString;
+
+ public CustomClassF() {}
+ public CustomClassF(List> listOfString) {
+ this.listOfString = listOfString;
+ }
+
+ @Override
+ public boolean equals(Object o) {
+ CustomClassF classF = (CustomClassF) o;
+ return this.listOfString.equals(classF.listOfString);
+ }
+}
+
diff --git a/src/test/java/org/json/junit/data/CustomClassG.java b/src/test/java/org/json/junit/data/CustomClassG.java
new file mode 100644
index 000000000..c8c9f5784
--- /dev/null
+++ b/src/test/java/org/json/junit/data/CustomClassG.java
@@ -0,0 +1,18 @@
+package org.json.junit.data;
+
+import java.util.Map;
+
+public class CustomClassG {
+ public Map dataList;
+
+ public CustomClassG () {}
+ public CustomClassG (Map dataList) {
+ this.dataList = dataList;
+ }
+
+ @Override
+ public boolean equals(Object object) {
+ CustomClassG classG = (CustomClassG) object;
+ return this.dataList.equals(classG.dataList);
+ }
+}
diff --git a/src/test/java/org/json/junit/data/CustomClassH.java b/src/test/java/org/json/junit/data/CustomClassH.java
new file mode 100644
index 000000000..ce9b1af23
--- /dev/null
+++ b/src/test/java/org/json/junit/data/CustomClassH.java
@@ -0,0 +1,22 @@
+package org.json.junit.data;
+
+import java.util.Map;
+import java.util.List;
+import java.util.ArrayList;
+
+public class CustomClassH {
+ public Map> integerMap;
+
+ public CustomClassH() {}
+ public CustomClassH(Map> integerMap) {
+ this.integerMap = integerMap;
+ }
+
+ @Override
+ public boolean equals(Object object) {
+ CustomClassH classH = (CustomClassH) object;
+ return this.integerMap.size() == classH.integerMap.size()
+ && this.integerMap.keySet().equals(classH.integerMap.keySet())
+ && new ArrayList<>(this.integerMap.values()).equals(new ArrayList<>(classH.integerMap.values()));
+ }
+}
diff --git a/src/test/java/org/json/junit/data/CustomClassI.java b/src/test/java/org/json/junit/data/CustomClassI.java
new file mode 100644
index 000000000..bd7c4ed89
--- /dev/null
+++ b/src/test/java/org/json/junit/data/CustomClassI.java
@@ -0,0 +1,12 @@
+package org.json.junit.data;
+
+import java.util.Map;
+
+public class CustomClassI {
+ public Map> integerMap;
+
+ public CustomClassI() {}
+ public CustomClassI(Map> integerMap) {
+ this.integerMap = integerMap;
+ }
+}
diff --git a/src/test/java/org/json/junit/data/PersonRecord.java b/src/test/java/org/json/junit/data/PersonRecord.java
new file mode 100644
index 000000000..891f1bb9e
--- /dev/null
+++ b/src/test/java/org/json/junit/data/PersonRecord.java
@@ -0,0 +1,31 @@
+package org.json.junit.data;
+
+/**
+ * A test class that mimics Java record accessor patterns.
+ * Records use accessor methods without get/is prefixes (e.g., name() instead of getName()).
+ * This class simulates that behavior to test JSONObject's handling of such methods.
+ */
+public class PersonRecord {
+ private final String name;
+ private final int age;
+ private final boolean active;
+
+ public PersonRecord(String name, int age, boolean active) {
+ this.name = name;
+ this.age = age;
+ this.active = active;
+ }
+
+ // Record-style accessors (no "get" or "is" prefix)
+ public String name() {
+ return name;
+ }
+
+ public int age() {
+ return age;
+ }
+
+ public boolean active() {
+ return active;
+ }
+}
diff --git a/src/test/resources/JSONArrayExpectedTestCaseForToJsonArrayTest.json b/src/test/resources/JSONArrayExpectedTestCaseForToJsonArrayTest.json
deleted file mode 100644
index 6ce0864a2..000000000
--- a/src/test/resources/JSONArrayExpectedTestCaseForToJsonArrayTest.json
+++ /dev/null
@@ -1,91 +0,0 @@
-[
- "addresses",
- {
- "xsi:noNamespaceSchemaLocation": "test.xsd",
- "xmlns:xsi": "http://www.w3.org/2001/XMLSchema-instance"
- },
- [
- "address",
- {
- "addrType": "my address"
- },
- [
- "name",
- {
- "nameType": "my name"
- },
- "Joe Tester"
- ],
- [
- "street",
- "Baker street 5"
- ],
- [
- "NothingHere",
- {
- "except": "an attribute"
- }
- ],
- [
- "TrueValue",
- true
- ],
- [
- "FalseValue",
- false
- ],
- [
- "NullValue",
- null
- ],
- [
- "PositiveValue",
- 42
- ],
- [
- "NegativeValue",
- -23
- ],
- [
- "DoubleValue",
- -23.45
- ],
- [
- "Nan",
- "-23x.45"
- ],
- [
- "ArrayOfNum",
- [
- "value",
- 1
- ],
- [
- "value",
- 2
- ],
- [
- "value",
- [
- "subValue",
- {
- "svAttr": "svValue"
- },
- "abc"
- ]
- ],
- [
- "value",
- 3
- ],
- [
- "value",
- 4.1
- ],
- [
- "value",
- 5.2
- ]
- ]
- ]
-]
diff --git a/src/test/resources/XmlTestCaseTestToJsonArray.xml b/src/test/resources/XmlTestCaseTestToJsonArray.xml
deleted file mode 100644
index dfcf9d90c..000000000
--- a/src/test/resources/XmlTestCaseTestToJsonArray.xml
+++ /dev/null
@@ -1,27 +0,0 @@
-
-
-
-
- Joe Tester
-
-
- true
- false
- null
- 42
- -23
- -23.45
- -23x.45
-
- 1
- 2
-
- abc
-
- 3
- 4.1
- 5.2
-
-
-
\ No newline at end of file
diff --git a/src/test/resources/compliantJsonArray.json b/src/test/resources/compliantJsonArray.json
index c37369027..d68c99588 100644
--- a/src/test/resources/compliantJsonArray.json
+++ b/src/test/resources/compliantJsonArray.json
@@ -314,4 +314,4 @@
"greeting": "Hello, Sample! You have 6 unread messages.",
"favoriteFruit": "apple"
}
-]
\ No newline at end of file
+]
diff --git a/src/test/resources/compliantJsonObject.json b/src/test/resources/compliantJsonObject.json
new file mode 100644
index 000000000..cb2918d37
--- /dev/null
+++ b/src/test/resources/compliantJsonObject.json
@@ -0,0 +1,3703 @@
+{
+ "a0": [
+ {
+ "id": 0,
+ "name": "Elijah",
+ "city": "Austin",
+ "age": 78,
+ "friends": [
+ {
+ "name": "Michelle",
+ "hobbies": [
+ "Watching Sports",
+ "Reading",
+ "Skiing & Snowboarding"
+ ]
+ },
+ {
+ "name": "Robert",
+ "hobbies": [
+ "Traveling",
+ "Video Games"
+ ]
+ }
+ ]
+ },
+ {
+ "id": 1,
+ "name": "Noah",
+ "city": "Boston",
+ "age": 97,
+ "friends": [
+ {
+ "name": "Oliver",
+ "hobbies": [
+ "Watching Sports",
+ "Skiing & Snowboarding",
+ "Collecting"
+ ]
+ },
+ {
+ "name": "Olivia",
+ "hobbies": [
+ "Running",
+ "Music",
+ "Woodworking"
+ ]
+ },
+ {
+ "name": "Robert",
+ "hobbies": [
+ "Woodworking",
+ "Calligraphy",
+ "Genealogy"
+ ]
+ },
+ {
+ "name": "Ava",
+ "hobbies": [
+ "Walking",
+ "Church Activities"
+ ]
+ },
+ {
+ "name": "Michael",
+ "hobbies": [
+ "Music",
+ "Church Activities"
+ ]
+ },
+ {
+ "name": "Michael",
+ "hobbies": [
+ "Martial Arts",
+ "Painting",
+ "Jewelry Making"
+ ]
+ }
+ ]
+ },
+ {
+ "id": 2,
+ "name": "Evy",
+ "city": "San Diego",
+ "age": 48,
+ "friends": [
+ {
+ "name": "Joe",
+ "hobbies": [
+ "Reading",
+ "Volunteer Work"
+ ]
+ },
+ {
+ "name": "Joe",
+ "hobbies": [
+ "Genealogy",
+ "Golf"
+ ]
+ },
+ {
+ "name": "Oliver",
+ "hobbies": [
+ "Collecting",
+ "Writing",
+ "Bicycling"
+ ]
+ },
+ {
+ "name": "Liam",
+ "hobbies": [
+ "Church Activities",
+ "Jewelry Making"
+ ]
+ },
+ {
+ "name": "Amelia",
+ "hobbies": [
+ "Calligraphy",
+ "Dancing"
+ ]
+ }
+ ]
+ },
+ {
+ "id": 3,
+ "name": "Oliver",
+ "city": "St. Louis",
+ "age": 39,
+ "friends": [
+ {
+ "name": "Mateo",
+ "hobbies": [
+ "Watching Sports",
+ "Gardening"
+ ]
+ },
+ {
+ "name": "Nora",
+ "hobbies": [
+ "Traveling",
+ "Team Sports"
+ ]
+ },
+ {
+ "name": "Ava",
+ "hobbies": [
+ "Church Activities",
+ "Running"
+ ]
+ },
+ {
+ "name": "Amelia",
+ "hobbies": [
+ "Gardening",
+ "Board Games",
+ "Watching Sports"
+ ]
+ },
+ {
+ "name": "Leo",
+ "hobbies": [
+ "Martial Arts",
+ "Video Games",
+ "Reading"
+ ]
+ }
+ ]
+ },
+ {
+ "id": 4,
+ "name": "Michael",
+ "city": "St. Louis",
+ "age": 95,
+ "friends": [
+ {
+ "name": "Mateo",
+ "hobbies": [
+ "Movie Watching",
+ "Collecting"
+ ]
+ },
+ {
+ "name": "Chris",
+ "hobbies": [
+ "Housework",
+ "Bicycling",
+ "Collecting"
+ ]
+ }
+ ]
+ },
+ {
+ "id": 5,
+ "name": "Michael",
+ "city": "Portland",
+ "age": 19,
+ "friends": [
+ {
+ "name": "Jack",
+ "hobbies": [
+ "Painting",
+ "Television"
+ ]
+ },
+ {
+ "name": "Oliver",
+ "hobbies": [
+ "Walking",
+ "Watching Sports",
+ "Movie Watching"
+ ]
+ },
+ {
+ "name": "Charlotte",
+ "hobbies": [
+ "Podcasts",
+ "Jewelry Making"
+ ]
+ },
+ {
+ "name": "Elijah",
+ "hobbies": [
+ "Eating Out",
+ "Painting"
+ ]
+ }
+ ]
+ },
+ {
+ "id": 6,
+ "name": "Lucas",
+ "city": "Austin",
+ "age": 76,
+ "friends": [
+ {
+ "name": "John",
+ "hobbies": [
+ "Genealogy",
+ "Cooking"
+ ]
+ },
+ {
+ "name": "John",
+ "hobbies": [
+ "Socializing",
+ "Yoga"
+ ]
+ }
+ ]
+ },
+ {
+ "id": 7,
+ "name": "Michelle",
+ "city": "San Antonio",
+ "age": 25,
+ "friends": [
+ {
+ "name": "Jack",
+ "hobbies": [
+ "Music",
+ "Golf"
+ ]
+ },
+ {
+ "name": "Daniel",
+ "hobbies": [
+ "Socializing",
+ "Housework",
+ "Walking"
+ ]
+ },
+ {
+ "name": "Robert",
+ "hobbies": [
+ "Collecting",
+ "Walking"
+ ]
+ },
+ {
+ "name": "Nora",
+ "hobbies": [
+ "Painting",
+ "Church Activities"
+ ]
+ },
+ {
+ "name": "Mia",
+ "hobbies": [
+ "Running",
+ "Painting"
+ ]
+ }
+ ]
+ },
+ {
+ "id": 8,
+ "name": "Emily",
+ "city": "Austin",
+ "age": 61,
+ "friends": [
+ {
+ "name": "Nora",
+ "hobbies": [
+ "Bicycling",
+ "Skiing & Snowboarding",
+ "Watching Sports"
+ ]
+ },
+ {
+ "name": "Ava",
+ "hobbies": [
+ "Writing",
+ "Reading",
+ "Collecting"
+ ]
+ },
+ {
+ "name": "Amelia",
+ "hobbies": [
+ "Eating Out",
+ "Watching Sports"
+ ]
+ },
+ {
+ "name": "Daniel",
+ "hobbies": [
+ "Skiing & Snowboarding",
+ "Martial Arts",
+ "Writing"
+ ]
+ },
+ {
+ "name": "Zoey",
+ "hobbies": [
+ "Board Games",
+ "Tennis"
+ ]
+ }
+ ]
+ },
+ {
+ "id": 9,
+ "name": "Liam",
+ "city": "New Orleans",
+ "age": 33,
+ "friends": [
+ {
+ "name": "Chloe",
+ "hobbies": [
+ "Traveling",
+ "Bicycling",
+ "Shopping"
+ ]
+ },
+ {
+ "name": "Evy",
+ "hobbies": [
+ "Eating Out",
+ "Watching Sports"
+ ]
+ },
+ {
+ "name": "Grace",
+ "hobbies": [
+ "Jewelry Making",
+ "Yoga",
+ "Podcasts"
+ ]
+ }
+ ]
+ },
+ {
+ "id": 10,
+ "name": "Levi",
+ "city": "New Orleans",
+ "age": 59,
+ "friends": [
+ {
+ "name": "Noah",
+ "hobbies": [
+ "Video Games",
+ "Fishing",
+ "Shopping"
+ ]
+ },
+ {
+ "name": "Sarah",
+ "hobbies": [
+ "Woodworking",
+ "Music",
+ "Reading"
+ ]
+ }
+ ]
+ },
+ {
+ "id": 11,
+ "name": "Lucas",
+ "city": "Portland",
+ "age": 82,
+ "friends": [
+ {
+ "name": "Luke",
+ "hobbies": [
+ "Jewelry Making",
+ "Yoga"
+ ]
+ },
+ {
+ "name": "Noah",
+ "hobbies": [
+ "Fishing",
+ "Movie Watching"
+ ]
+ },
+ {
+ "name": "Evy",
+ "hobbies": [
+ "Gardening",
+ "Church Activities",
+ "Fishing"
+ ]
+ }
+ ]
+ },
+ {
+ "id": 12,
+ "name": "Kevin",
+ "city": "Charleston",
+ "age": 82,
+ "friends": [
+ {
+ "name": "Oliver",
+ "hobbies": [
+ "Eating Out"
+ ]
+ },
+ {
+ "name": "Michael",
+ "hobbies": [
+ "Fishing",
+ "Writing"
+ ]
+ }
+ ]
+ },
+ {
+ "id": 13,
+ "name": "Olivia",
+ "city": "San Antonio",
+ "age": 34,
+ "friends": [
+ {
+ "name": "Daniel",
+ "hobbies": [
+ "Yoga",
+ "Traveling",
+ "Movie Watching"
+ ]
+ },
+ {
+ "name": "Zoey",
+ "hobbies": [
+ "Team Sports",
+ "Writing"
+ ]
+ }
+ ]
+ },
+ {
+ "id": 14,
+ "name": "Robert",
+ "city": "Los Angeles",
+ "age": 49,
+ "friends": [
+ {
+ "name": "Michelle",
+ "hobbies": [
+ "Yoga",
+ "Television"
+ ]
+ },
+ {
+ "name": "Daniel",
+ "hobbies": [
+ "Fishing",
+ "Martial Arts"
+ ]
+ },
+ {
+ "name": "Chloe",
+ "hobbies": [
+ "Church Activities",
+ "Television"
+ ]
+ },
+ {
+ "name": "Sarah",
+ "hobbies": [
+ "Movie Watching",
+ "Playing Cards"
+ ]
+ },
+ {
+ "name": "Kevin",
+ "hobbies": [
+ "Golf",
+ "Running",
+ "Cooking"
+ ]
+ }
+ ]
+ },
+ {
+ "id": 15,
+ "name": "Grace",
+ "city": "Chicago",
+ "age": 98,
+ "friends": [
+ {
+ "name": "Joe",
+ "hobbies": [
+ "Traveling",
+ "Genealogy"
+ ]
+ },
+ {
+ "name": "Mateo",
+ "hobbies": [
+ "Golf",
+ "Podcasts"
+ ]
+ },
+ {
+ "name": "Mateo",
+ "hobbies": [
+ "Reading",
+ "Cooking"
+ ]
+ }
+ ]
+ },
+ {
+ "id": 16,
+ "name": "Michael",
+ "city": "New Orleans",
+ "age": 78,
+ "friends": [
+ {
+ "name": "Amelia",
+ "hobbies": [
+ "Running",
+ "Housework",
+ "Gardening"
+ ]
+ },
+ {
+ "name": "Michael",
+ "hobbies": [
+ "Writing",
+ "Golf"
+ ]
+ },
+ {
+ "name": "Leo",
+ "hobbies": [
+ "Running",
+ "Church Activities"
+ ]
+ },
+ {
+ "name": "Elijah",
+ "hobbies": [
+ "Volunteer Work",
+ "Eating Out"
+ ]
+ },
+ {
+ "name": "Mateo",
+ "hobbies": [
+ "Socializing",
+ "Watching Sports",
+ "Collecting"
+ ]
+ },
+ {
+ "name": "Levi",
+ "hobbies": [
+ "Eating Out",
+ "Walking"
+ ]
+ }
+ ]
+ },
+ {
+ "id": 17,
+ "name": "Mateo",
+ "city": "Palm Springs",
+ "age": 19,
+ "friends": [
+ {
+ "name": "Emily",
+ "hobbies": [
+ "Playing Cards",
+ "Walking"
+ ]
+ },
+ {
+ "name": "Sophie",
+ "hobbies": [
+ "Gardening",
+ "Board Games",
+ "Volunteer Work"
+ ]
+ },
+ {
+ "name": "Camila",
+ "hobbies": [
+ "Board Games",
+ "Dancing"
+ ]
+ },
+ {
+ "name": "John",
+ "hobbies": [
+ "Golf",
+ "Playing Cards",
+ "Music"
+ ]
+ }
+ ]
+ },
+ {
+ "id": 18,
+ "name": "Levi",
+ "city": "Chicago",
+ "age": 38,
+ "friends": [
+ {
+ "name": "Emma",
+ "hobbies": [
+ "Tennis",
+ "Eating Out"
+ ]
+ },
+ {
+ "name": "Emma",
+ "hobbies": [
+ "Writing",
+ "Reading",
+ "Eating Out"
+ ]
+ },
+ {
+ "name": "Daniel",
+ "hobbies": [
+ "Collecting",
+ "Video Games"
+ ]
+ },
+ {
+ "name": "Evy",
+ "hobbies": [
+ "Shopping",
+ "Walking"
+ ]
+ },
+ {
+ "name": "Sophie",
+ "hobbies": [
+ "Dancing",
+ "Volunteer Work"
+ ]
+ },
+ {
+ "name": "Mia",
+ "hobbies": [
+ "Podcasts",
+ "Woodworking",
+ "Martial Arts"
+ ]
+ }
+ ]
+ },
+ {
+ "id": 19,
+ "name": "Luke",
+ "city": "New York City",
+ "age": 49,
+ "friends": [
+ {
+ "name": "Leo",
+ "hobbies": [
+ "Writing",
+ "Playing Cards",
+ "Housework"
+ ]
+ },
+ {
+ "name": "Emma",
+ "hobbies": [
+ "Gardening",
+ "Running"
+ ]
+ },
+ {
+ "name": "Sarah",
+ "hobbies": [
+ "Golf",
+ "Music"
+ ]
+ },
+ {
+ "name": "Jack",
+ "hobbies": [
+ "Board Games",
+ "Socializing",
+ "Writing"
+ ]
+ },
+ {
+ "name": "Jack",
+ "hobbies": [
+ "Movie Watching",
+ "Writing",
+ "Fishing"
+ ]
+ },
+ {
+ "name": "Michael",
+ "hobbies": [
+ "Golf",
+ "Jewelry Making",
+ "Yoga"
+ ]
+ }
+ ]
+ },
+ {
+ "id": 20,
+ "name": "Camila",
+ "city": "New Orleans",
+ "age": 69,
+ "friends": [
+ {
+ "name": "Kevin",
+ "hobbies": [
+ "Video Games",
+ "Collecting",
+ "Painting"
+ ]
+ },
+ {
+ "name": "Grace",
+ "hobbies": [
+ "Reading",
+ "Volunteer Work"
+ ]
+ }
+ ]
+ },
+ {
+ "id": 21,
+ "name": "Amelia",
+ "city": "Charleston",
+ "age": 70,
+ "friends": [
+ {
+ "name": "John",
+ "hobbies": [
+ "Quilting",
+ "Volunteer Work"
+ ]
+ },
+ {
+ "name": "Leo",
+ "hobbies": [
+ "Painting",
+ "Podcasts"
+ ]
+ }
+ ]
+ },
+ {
+ "id": 22,
+ "name": "Victoria",
+ "city": "Miami Beach",
+ "age": 50,
+ "friends": [
+ {
+ "name": "Mia",
+ "hobbies": [
+ "Cooking",
+ "Team Sports"
+ ]
+ },
+ {
+ "name": "Lucas",
+ "hobbies": [
+ "Team Sports",
+ "Genealogy"
+ ]
+ }
+ ]
+ },
+ {
+ "id": 23,
+ "name": "Kevin",
+ "city": "Miami Beach",
+ "age": 93,
+ "friends": [
+ {
+ "name": "Jack",
+ "hobbies": [
+ "Bicycling",
+ "Fishing"
+ ]
+ },
+ {
+ "name": "Sophie",
+ "hobbies": [
+ "Martial Arts",
+ "Genealogy",
+ "Tennis"
+ ]
+ },
+ {
+ "name": "Chloe",
+ "hobbies": [
+ "Yoga"
+ ]
+ }
+ ]
+ },
+ {
+ "id": 24,
+ "name": "Daniel",
+ "city": "Saint Augustine",
+ "age": 43,
+ "friends": [
+ {
+ "name": "Sarah",
+ "hobbies": [
+ "Calligraphy",
+ "Martial Arts",
+ "Music"
+ ]
+ },
+ {
+ "name": "Evy",
+ "hobbies": [
+ "Walking",
+ "Bicycling"
+ ]
+ },
+ {
+ "name": "Kevin",
+ "hobbies": [
+ "Collecting",
+ "Golf"
+ ]
+ },
+ {
+ "name": "Mia",
+ "hobbies": [
+ "Podcasts",
+ "Walking"
+ ]
+ }
+ ]
+ },
+ {
+ "id": 25,
+ "name": "Olivia",
+ "city": "Austin",
+ "age": 46,
+ "friends": [
+ {
+ "name": "Mia",
+ "hobbies": [
+ "Podcasts",
+ "Housework"
+ ]
+ },
+ {
+ "name": "Robert",
+ "hobbies": [
+ "Golf",
+ "Volunteer Work"
+ ]
+ },
+ {
+ "name": "Michelle",
+ "hobbies": [
+ "Eating Out",
+ "Music"
+ ]
+ },
+ {
+ "name": "Elijah",
+ "hobbies": [
+ "Eating Out",
+ "Genealogy",
+ "Reading"
+ ]
+ }
+ ]
+ },
+ {
+ "id": 26,
+ "name": "Michael",
+ "city": "Palm Springs",
+ "age": 62,
+ "friends": [
+ {
+ "name": "Sarah",
+ "hobbies": [
+ "Socializing",
+ "Playing Cards"
+ ]
+ },
+ {
+ "name": "Daniel",
+ "hobbies": [
+ "Playing Cards",
+ "Shopping",
+ "Collecting"
+ ]
+ },
+ {
+ "name": "Chloe",
+ "hobbies": [
+ "Music",
+ "Movie Watching"
+ ]
+ },
+ {
+ "name": "Zoey",
+ "hobbies": [
+ "Volunteer Work",
+ "Calligraphy",
+ "Jewelry Making"
+ ]
+ }
+ ]
+ },
+ {
+ "id": 27,
+ "name": "Kevin",
+ "city": "San Antonio",
+ "age": 97,
+ "friends": [
+ {
+ "name": "Sarah",
+ "hobbies": [
+ "Television",
+ "Quilting",
+ "Team Sports"
+ ]
+ },
+ {
+ "name": "Oliver",
+ "hobbies": [
+ "Shopping",
+ "Martial Arts"
+ ]
+ }
+ ]
+ },
+ {
+ "id": 28,
+ "name": "Oliver",
+ "city": "Honolulu",
+ "age": 79,
+ "friends": [
+ {
+ "name": "Charlotte",
+ "hobbies": [
+ "Housework",
+ "Jewelry Making"
+ ]
+ },
+ {
+ "name": "Isabella",
+ "hobbies": [
+ "Volunteer Work",
+ "Movie Watching"
+ ]
+ },
+ {
+ "name": "Ava",
+ "hobbies": [
+ "Traveling",
+ "Bicycling"
+ ]
+ },
+ {
+ "name": "Chris",
+ "hobbies": [
+ "Shopping",
+ "Church Activities",
+ "Dancing"
+ ]
+ },
+ {
+ "name": "Sarah",
+ "hobbies": [
+ "Reading",
+ "Movie Watching"
+ ]
+ },
+ {
+ "name": "Daniel",
+ "hobbies": [
+ "Socializing",
+ "Collecting",
+ "Cooking"
+ ]
+ }
+ ]
+ },
+ {
+ "id": 29,
+ "name": "Levi",
+ "city": "Miami Beach",
+ "age": 46,
+ "friends": [
+ {
+ "name": "Ava",
+ "hobbies": [
+ "Housework",
+ "Video Games",
+ "Walking"
+ ]
+ },
+ {
+ "name": "Elijah",
+ "hobbies": [
+ "Golf",
+ "Volunteer Work",
+ "Painting"
+ ]
+ },
+ {
+ "name": "Lucas",
+ "hobbies": [
+ "Writing",
+ "Martial Arts",
+ "Television"
+ ]
+ }
+ ]
+ },
+ {
+ "id": 30,
+ "name": "Michael",
+ "city": "Seattle",
+ "age": 18,
+ "friends": [
+ {
+ "name": "Oliver",
+ "hobbies": [
+ "Shopping",
+ "Woodworking"
+ ]
+ },
+ {
+ "name": "Emma",
+ "hobbies": [
+ "Yoga",
+ "Genealogy",
+ "Traveling"
+ ]
+ },
+ {
+ "name": "Evy",
+ "hobbies": [
+ "Eating Out",
+ "Church Activities",
+ "Calligraphy"
+ ]
+ },
+ {
+ "name": "Noah",
+ "hobbies": [
+ "Board Games",
+ "Television"
+ ]
+ }
+ ]
+ },
+ {
+ "id": 31,
+ "name": "Isabella",
+ "city": "Savannah",
+ "age": 65,
+ "friends": [
+ {
+ "name": "Jack",
+ "hobbies": [
+ "Church Activities",
+ "Housework",
+ "Martial Arts"
+ ]
+ },
+ {
+ "name": "Oliver",
+ "hobbies": [
+ "Calligraphy",
+ "Cooking"
+ ]
+ },
+ {
+ "name": "Grace",
+ "hobbies": [
+ "Volunteer Work",
+ "Podcasts",
+ "Tennis"
+ ]
+ }
+ ]
+ },
+ {
+ "id": 32,
+ "name": "Chris",
+ "city": "Las Vegas",
+ "age": 31,
+ "friends": [
+ {
+ "name": "Levi",
+ "hobbies": [
+ "Shopping",
+ "Fishing"
+ ]
+ },
+ {
+ "name": "Nora",
+ "hobbies": [
+ "Dancing",
+ "Quilting"
+ ]
+ },
+ {
+ "name": "Levi",
+ "hobbies": [
+ "Reading",
+ "Eating Out"
+ ]
+ },
+ {
+ "name": "Zoey",
+ "hobbies": [
+ "Traveling",
+ "Golf",
+ "Genealogy"
+ ]
+ },
+ {
+ "name": "Mia",
+ "hobbies": [
+ "Video Games",
+ "Shopping",
+ "Walking"
+ ]
+ }
+ ]
+ },
+ {
+ "id": 33,
+ "name": "Kevin",
+ "city": "Portland",
+ "age": 51,
+ "friends": [
+ {
+ "name": "Olivia",
+ "hobbies": [
+ "Running",
+ "Calligraphy"
+ ]
+ },
+ {
+ "name": "Amelia",
+ "hobbies": [
+ "Tennis",
+ "Genealogy"
+ ]
+ }
+ ]
+ },
+ {
+ "id": 34,
+ "name": "Sophie",
+ "city": "New York City",
+ "age": 25,
+ "friends": [
+ {
+ "name": "Levi",
+ "hobbies": [
+ "Video Games",
+ "Board Games",
+ "Podcasts"
+ ]
+ },
+ {
+ "name": "Joe",
+ "hobbies": [
+ "Calligraphy",
+ "Video Games",
+ "Martial Arts"
+ ]
+ }
+ ]
+ },
+ {
+ "id": 35,
+ "name": "John",
+ "city": "Orlando",
+ "age": 67,
+ "friends": [
+ {
+ "name": "Nora",
+ "hobbies": [
+ "Podcasts",
+ "Skiing & Snowboarding",
+ "Quilting"
+ ]
+ },
+ {
+ "name": "Jack",
+ "hobbies": [
+ "Tennis",
+ "Socializing",
+ "Music"
+ ]
+ },
+ {
+ "name": "Camila",
+ "hobbies": [
+ "Walking",
+ "Church Activities",
+ "Quilting"
+ ]
+ },
+ {
+ "name": "Grace",
+ "hobbies": [
+ "Team Sports",
+ "Cooking"
+ ]
+ },
+ {
+ "name": "Isabella",
+ "hobbies": [
+ "Skiing & Snowboarding",
+ "Dancing",
+ "Painting"
+ ]
+ },
+ {
+ "name": "Ava",
+ "hobbies": [
+ "Tennis",
+ "Bicycling"
+ ]
+ }
+ ]
+ },
+ {
+ "id": 36,
+ "name": "Emily",
+ "city": "New York City",
+ "age": 82,
+ "friends": [
+ {
+ "name": "Emma",
+ "hobbies": [
+ "Church Activities",
+ "Writing"
+ ]
+ },
+ {
+ "name": "Luke",
+ "hobbies": [
+ "Running",
+ "Calligraphy",
+ "Tennis"
+ ]
+ },
+ {
+ "name": "Robert",
+ "hobbies": [
+ "Dancing",
+ "Socializing"
+ ]
+ },
+ {
+ "name": "Amelia",
+ "hobbies": [
+ "Genealogy",
+ "Calligraphy",
+ "Tennis"
+ ]
+ }
+ ]
+ },
+ {
+ "id": 37,
+ "name": "Amelia",
+ "city": "New Orleans",
+ "age": 28,
+ "friends": [
+ {
+ "name": "Victoria",
+ "hobbies": [
+ "Traveling",
+ "Skiing & Snowboarding"
+ ]
+ },
+ {
+ "name": "Luke",
+ "hobbies": [
+ "Martial Arts",
+ "Cooking",
+ "Dancing"
+ ]
+ },
+ {
+ "name": "Olivia",
+ "hobbies": [
+ "Bicycling",
+ "Walking",
+ "Podcasts"
+ ]
+ },
+ {
+ "name": "Elijah",
+ "hobbies": [
+ "Traveling",
+ "Volunteer Work",
+ "Collecting"
+ ]
+ }
+ ]
+ },
+ {
+ "id": 38,
+ "name": "Victoria",
+ "city": "Austin",
+ "age": 71,
+ "friends": [
+ {
+ "name": "Noah",
+ "hobbies": [
+ "Yoga",
+ "Shopping"
+ ]
+ },
+ {
+ "name": "Evy",
+ "hobbies": [
+ "Eating Out",
+ "Writing",
+ "Shopping"
+ ]
+ },
+ {
+ "name": "Camila",
+ "hobbies": [
+ "Volunteer Work",
+ "Team Sports"
+ ]
+ },
+ {
+ "name": "Lucas",
+ "hobbies": [
+ "Volunteer Work",
+ "Board Games",
+ "Running"
+ ]
+ }
+ ]
+ },
+ {
+ "id": 39,
+ "name": "Mia",
+ "city": "Honolulu",
+ "age": 63,
+ "friends": [
+ {
+ "name": "Sophie",
+ "hobbies": [
+ "Volunteer Work",
+ "Housework",
+ "Bicycling"
+ ]
+ },
+ {
+ "name": "Evy",
+ "hobbies": [
+ "Woodworking",
+ "Golf"
+ ]
+ },
+ {
+ "name": "Ava",
+ "hobbies": [
+ "Martial Arts",
+ "Skiing & Snowboarding",
+ "Dancing"
+ ]
+ },
+ {
+ "name": "Joe",
+ "hobbies": [
+ "Collecting",
+ "Watching Sports"
+ ]
+ },
+ {
+ "name": "Camila",
+ "hobbies": [
+ "Television",
+ "Socializing",
+ "Skiing & Snowboarding"
+ ]
+ },
+ {
+ "name": "Robert",
+ "hobbies": [
+ "Martial Arts",
+ "Woodworking",
+ "Reading"
+ ]
+ }
+ ]
+ },
+ {
+ "id": 40,
+ "name": "Daniel",
+ "city": "Las Vegas",
+ "age": 50,
+ "friends": [
+ {
+ "name": "Kevin",
+ "hobbies": [
+ "Bicycling",
+ "Housework"
+ ]
+ },
+ {
+ "name": "Evy",
+ "hobbies": [
+ "Woodworking",
+ "Collecting"
+ ]
+ }
+ ]
+ },
+ {
+ "id": 41,
+ "name": "Luke",
+ "city": "Nashville",
+ "age": 84,
+ "friends": [
+ {
+ "name": "Lucas",
+ "hobbies": [
+ "Fishing",
+ "Shopping"
+ ]
+ },
+ {
+ "name": "Victoria",
+ "hobbies": [
+ "Church Activities",
+ "Martial Arts",
+ "Television"
+ ]
+ },
+ {
+ "name": "Charlotte",
+ "hobbies": [
+ "Church Activities",
+ "Walking"
+ ]
+ },
+ {
+ "name": "Evy",
+ "hobbies": [
+ "Calligraphy",
+ "Writing"
+ ]
+ },
+ {
+ "name": "Liam",
+ "hobbies": [
+ "Movie Watching",
+ "Board Games"
+ ]
+ }
+ ]
+ },
+ {
+ "id": 42,
+ "name": "Joe",
+ "city": "Orlando",
+ "age": 28,
+ "friends": [
+ {
+ "name": "Sophie",
+ "hobbies": [
+ "Board Games",
+ "Music"
+ ]
+ },
+ {
+ "name": "Camila",
+ "hobbies": [
+ "Woodworking",
+ "Yoga",
+ "Music"
+ ]
+ },
+ {
+ "name": "Jack",
+ "hobbies": [
+ "Team Sports",
+ "Bicycling"
+ ]
+ }
+ ]
+ },
+ {
+ "id": 43,
+ "name": "Robert",
+ "city": "Boston",
+ "age": 89,
+ "friends": [
+ {
+ "name": "Mia",
+ "hobbies": [
+ "Team Sports",
+ "Church Activities",
+ "Martial Arts"
+ ]
+ },
+ {
+ "name": "Elijah",
+ "hobbies": [
+ "Housework",
+ "Collecting"
+ ]
+ },
+ {
+ "name": "Zoey",
+ "hobbies": [
+ "Watching Sports",
+ "Golf",
+ "Podcasts"
+ ]
+ },
+ {
+ "name": "Sarah",
+ "hobbies": [
+ "Volunteer Work",
+ "Team Sports"
+ ]
+ },
+ {
+ "name": "Chloe",
+ "hobbies": [
+ "Yoga",
+ "Walking"
+ ]
+ },
+ {
+ "name": "Mateo",
+ "hobbies": [
+ "Running",
+ "Painting",
+ "Television"
+ ]
+ }
+ ]
+ },
+ {
+ "id": 44,
+ "name": "Mateo",
+ "city": "Palm Springs",
+ "age": 75,
+ "friends": [
+ {
+ "name": "Sarah",
+ "hobbies": [
+ "Socializing",
+ "Walking",
+ "Painting"
+ ]
+ },
+ {
+ "name": "Sophie",
+ "hobbies": [
+ "Skiing & Snowboarding",
+ "Bicycling",
+ "Eating Out"
+ ]
+ },
+ {
+ "name": "Zoey",
+ "hobbies": [
+ "Podcasts",
+ "Socializing",
+ "Calligraphy"
+ ]
+ },
+ {
+ "name": "Olivia",
+ "hobbies": [
+ "Dancing",
+ "Volunteer Work"
+ ]
+ },
+ {
+ "name": "Mia",
+ "hobbies": [
+ "Watching Sports",
+ "Yoga",
+ "Martial Arts"
+ ]
+ },
+ {
+ "name": "Mateo",
+ "hobbies": [
+ "Housework",
+ "Genealogy"
+ ]
+ }
+ ]
+ },
+ {
+ "id": 45,
+ "name": "Michelle",
+ "city": "Portland",
+ "age": 64,
+ "friends": [
+ {
+ "name": "Ava",
+ "hobbies": [
+ "Watching Sports",
+ "Shopping"
+ ]
+ },
+ {
+ "name": "John",
+ "hobbies": [
+ "Martial Arts",
+ "Video Games",
+ "Fishing"
+ ]
+ }
+ ]
+ },
+ {
+ "id": 46,
+ "name": "Emma",
+ "city": "Portland",
+ "age": 47,
+ "friends": [
+ {
+ "name": "Zoey",
+ "hobbies": [
+ "Yoga",
+ "Music",
+ "Bicycling"
+ ]
+ },
+ {
+ "name": "Olivia",
+ "hobbies": [
+ "Traveling",
+ "Movie Watching",
+ "Gardening"
+ ]
+ }
+ ]
+ },
+ {
+ "id": 47,
+ "name": "Elijah",
+ "city": "Chicago",
+ "age": 96,
+ "friends": [
+ {
+ "name": "Kevin",
+ "hobbies": [
+ "Video Games",
+ "Watching Sports",
+ "Eating Out"
+ ]
+ },
+ {
+ "name": "Kevin",
+ "hobbies": [
+ "Housework",
+ "Tennis",
+ "Playing Cards"
+ ]
+ },
+ {
+ "name": "Liam",
+ "hobbies": [
+ "Cooking"
+ ]
+ },
+ {
+ "name": "Kevin",
+ "hobbies": [
+ "Genealogy",
+ "Housework"
+ ]
+ }
+ ]
+ },
+ {
+ "id": 48,
+ "name": "Elijah",
+ "city": "Seattle",
+ "age": 30,
+ "friends": [
+ {
+ "name": "Kevin",
+ "hobbies": [
+ "Socializing",
+ "Eating Out"
+ ]
+ },
+ {
+ "name": "Olivia",
+ "hobbies": [
+ "Martial Arts",
+ "Golf",
+ "Cooking"
+ ]
+ },
+ {
+ "name": "Daniel",
+ "hobbies": [
+ "Gardening",
+ "Bicycling",
+ "Television"
+ ]
+ }
+ ]
+ },
+ {
+ "id": 49,
+ "name": "Sophie",
+ "city": "Palm Springs",
+ "age": 84,
+ "friends": [
+ {
+ "name": "Victoria",
+ "hobbies": [
+ "Podcasts",
+ "Martial Arts"
+ ]
+ },
+ {
+ "name": "Nora",
+ "hobbies": [
+ "Volunteer Work",
+ "Bicycling",
+ "Reading"
+ ]
+ },
+ {
+ "name": "Noah",
+ "hobbies": [
+ "Television",
+ "Watching Sports",
+ "Traveling"
+ ]
+ },
+ {
+ "name": "Victoria",
+ "hobbies": [
+ "Bicycling",
+ "Woodworking",
+ "Tennis"
+ ]
+ }
+ ]
+ },
+ {
+ "id": 50,
+ "name": "Sophie",
+ "city": "Chicago",
+ "age": 52,
+ "friends": [
+ {
+ "name": "Chris",
+ "hobbies": [
+ "Collecting",
+ "Dancing",
+ "Cooking"
+ ]
+ },
+ {
+ "name": "Emma",
+ "hobbies": [
+ "Watching Sports",
+ "Dancing",
+ "Tennis"
+ ]
+ },
+ {
+ "name": "Joe",
+ "hobbies": [
+ "Board Games",
+ "Skiing & Snowboarding"
+ ]
+ },
+ {
+ "name": "Jack",
+ "hobbies": [
+ "Calligraphy",
+ "Running"
+ ]
+ },
+ {
+ "name": "John",
+ "hobbies": [
+ "Quilting",
+ "Golf",
+ "Gardening"
+ ]
+ },
+ {
+ "name": "John",
+ "hobbies": [
+ "Watching Sports",
+ "Jewelry Making"
+ ]
+ }
+ ]
+ },
+ {
+ "id": 51,
+ "name": "Nora",
+ "city": "Lahaina",
+ "age": 79,
+ "friends": [
+ {
+ "name": "Elijah",
+ "hobbies": [
+ "Skiing & Snowboarding",
+ "Martial Arts"
+ ]
+ },
+ {
+ "name": "Kevin",
+ "hobbies": [
+ "Volunteer Work",
+ "Running",
+ "Tennis"
+ ]
+ },
+ {
+ "name": "Michael",
+ "hobbies": [
+ "Skiing & Snowboarding",
+ "Quilting",
+ "Fishing"
+ ]
+ }
+ ]
+ },
+ {
+ "id": 52,
+ "name": "Chris",
+ "city": "Miami Beach",
+ "age": 59,
+ "friends": [
+ {
+ "name": "Amelia",
+ "hobbies": [
+ "Video Games",
+ "Traveling",
+ "Cooking"
+ ]
+ },
+ {
+ "name": "Kevin",
+ "hobbies": [
+ "Shopping",
+ "Calligraphy"
+ ]
+ },
+ {
+ "name": "Luke",
+ "hobbies": [
+ "Playing Cards",
+ "Housework"
+ ]
+ },
+ {
+ "name": "Chloe",
+ "hobbies": [
+ "Painting",
+ "Housework",
+ "Shopping"
+ ]
+ }
+ ]
+ },
+ {
+ "id": 53,
+ "name": "Kevin",
+ "city": "Boston",
+ "age": 88,
+ "friends": [
+ {
+ "name": "Zoey",
+ "hobbies": [
+ "Dancing"
+ ]
+ },
+ {
+ "name": "Liam",
+ "hobbies": [
+ "Traveling",
+ "Martial Arts"
+ ]
+ },
+ {
+ "name": "Robert",
+ "hobbies": [
+ "Woodworking",
+ "Collecting"
+ ]
+ },
+ {
+ "name": "Elijah",
+ "hobbies": [
+ "Collecting",
+ "Running"
+ ]
+ },
+ {
+ "name": "Daniel",
+ "hobbies": [
+ "Dancing",
+ "Traveling"
+ ]
+ },
+ {
+ "name": "Emily",
+ "hobbies": [
+ "Fishing",
+ "Quilting",
+ "Team Sports"
+ ]
+ }
+ ]
+ },
+ {
+ "id": 54,
+ "name": "Grace",
+ "city": "Miami Beach",
+ "age": 62,
+ "friends": [
+ {
+ "name": "Joe",
+ "hobbies": [
+ "Church Activities",
+ "Music"
+ ]
+ },
+ {
+ "name": "Emily",
+ "hobbies": [
+ "Genealogy",
+ "Watching Sports",
+ "Woodworking"
+ ]
+ },
+ {
+ "name": "Chris",
+ "hobbies": [
+ "Team Sports",
+ "Skiing & Snowboarding"
+ ]
+ },
+ {
+ "name": "Grace",
+ "hobbies": [
+ "Yoga",
+ "Music",
+ "Running"
+ ]
+ }
+ ]
+ },
+ {
+ "id": 55,
+ "name": "Chloe",
+ "city": "Lahaina",
+ "age": 97,
+ "friends": [
+ {
+ "name": "Emily",
+ "hobbies": [
+ "Genealogy",
+ "Team Sports",
+ "Skiing & Snowboarding"
+ ]
+ },
+ {
+ "name": "Joe",
+ "hobbies": [
+ "Movie Watching",
+ "Television"
+ ]
+ }
+ ]
+ },
+ {
+ "id": 56,
+ "name": "Zoey",
+ "city": "Saint Augustine",
+ "age": 75,
+ "friends": [
+ {
+ "name": "Luke",
+ "hobbies": [
+ "Bicycling",
+ "Martial Arts"
+ ]
+ },
+ {
+ "name": "Emma",
+ "hobbies": [
+ "Music",
+ "Cooking"
+ ]
+ }
+ ]
+ },
+ {
+ "id": 57,
+ "name": "Sophie",
+ "city": "Boston",
+ "age": 26,
+ "friends": [
+ {
+ "name": "Levi",
+ "hobbies": [
+ "Writing",
+ "Yoga",
+ "Movie Watching"
+ ]
+ },
+ {
+ "name": "Noah",
+ "hobbies": [
+ "Board Games",
+ "Martial Arts",
+ "Shopping"
+ ]
+ },
+ {
+ "name": "Camila",
+ "hobbies": [
+ "Jewelry Making",
+ "Skiing & Snowboarding",
+ "Fishing"
+ ]
+ }
+ ]
+ },
+ {
+ "id": 58,
+ "name": "Emma",
+ "city": "Seattle",
+ "age": 40,
+ "friends": [
+ {
+ "name": "Victoria",
+ "hobbies": [
+ "Traveling",
+ "Bicycling"
+ ]
+ },
+ {
+ "name": "Michelle",
+ "hobbies": [
+ "Skiing & Snowboarding",
+ "Bicycling",
+ "Watching Sports"
+ ]
+ },
+ {
+ "name": "Michael",
+ "hobbies": [
+ "Board Games",
+ "Watching Sports"
+ ]
+ },
+ {
+ "name": "Daniel",
+ "hobbies": [
+ "Yoga",
+ "Shopping"
+ ]
+ }
+ ]
+ },
+ {
+ "id": 59,
+ "name": "Luke",
+ "city": "San Diego",
+ "age": 44,
+ "friends": [
+ {
+ "name": "Mia",
+ "hobbies": [
+ "Calligraphy",
+ "Writing"
+ ]
+ },
+ {
+ "name": "Michelle",
+ "hobbies": [
+ "Podcasts",
+ "Movie Watching",
+ "Playing Cards"
+ ]
+ },
+ {
+ "name": "Charlotte",
+ "hobbies": [
+ "Bicycling",
+ "Golf",
+ "Walking"
+ ]
+ }
+ ]
+ },
+ {
+ "id": 60,
+ "name": "Chloe",
+ "city": "Austin",
+ "age": 23,
+ "friends": [
+ {
+ "name": "Camila",
+ "hobbies": [
+ "Martial Arts",
+ "Golf"
+ ]
+ },
+ {
+ "name": "Sarah",
+ "hobbies": [
+ "Writing",
+ "Martial Arts",
+ "Jewelry Making"
+ ]
+ },
+ {
+ "name": "Amelia",
+ "hobbies": [
+ "Video Games",
+ "Bicycling",
+ "Eating Out"
+ ]
+ },
+ {
+ "name": "Sophie",
+ "hobbies": [
+ "Socializing",
+ "Collecting",
+ "Traveling"
+ ]
+ },
+ {
+ "name": "Olivia",
+ "hobbies": [
+ "Team Sports",
+ "Woodworking",
+ "Collecting"
+ ]
+ },
+ {
+ "name": "Elijah",
+ "hobbies": [
+ "Yoga",
+ "Music",
+ "Skiing & Snowboarding"
+ ]
+ }
+ ]
+ },
+ {
+ "id": 61,
+ "name": "Nora",
+ "city": "Orlando",
+ "age": 83,
+ "friends": [
+ {
+ "name": "Zoey",
+ "hobbies": [
+ "Board Games",
+ "Woodworking"
+ ]
+ },
+ {
+ "name": "Emma",
+ "hobbies": [
+ "Board Games",
+ "Traveling"
+ ]
+ },
+ {
+ "name": "Nora",
+ "hobbies": [
+ "Bicycling",
+ "Golf"
+ ]
+ },
+ {
+ "name": "Leo",
+ "hobbies": [
+ "Church Activities",
+ "Golf",
+ "Socializing"
+ ]
+ },
+ {
+ "name": "Oliver",
+ "hobbies": [
+ "Running",
+ "Dancing"
+ ]
+ },
+ {
+ "name": "Amelia",
+ "hobbies": [
+ "Board Games",
+ "Volunteer Work"
+ ]
+ }
+ ]
+ },
+ {
+ "id": 62,
+ "name": "Kevin",
+ "city": "Saint Augustine",
+ "age": 76,
+ "friends": [
+ {
+ "name": "Lucas",
+ "hobbies": [
+ "Playing Cards",
+ "Skiing & Snowboarding"
+ ]
+ },
+ {
+ "name": "Grace",
+ "hobbies": [
+ "Movie Watching",
+ "Calligraphy",
+ "Socializing"
+ ]
+ },
+ {
+ "name": "Ava",
+ "hobbies": [
+ "Podcasts",
+ "Yoga",
+ "Quilting"
+ ]
+ }
+ ]
+ },
+ {
+ "id": 63,
+ "name": "Amelia",
+ "city": "Honolulu",
+ "age": 84,
+ "friends": [
+ {
+ "name": "Grace",
+ "hobbies": [
+ "Golf",
+ "Reading"
+ ]
+ },
+ {
+ "name": "Luke",
+ "hobbies": [
+ "Genealogy",
+ "Martial Arts"
+ ]
+ },
+ {
+ "name": "Lucas",
+ "hobbies": [
+ "Gardening",
+ "Music"
+ ]
+ },
+ {
+ "name": "Isabella",
+ "hobbies": [
+ "Board Games",
+ "Music"
+ ]
+ },
+ {
+ "name": "Noah",
+ "hobbies": [
+ "Cooking",
+ "Eating Out",
+ "Quilting"
+ ]
+ },
+ {
+ "name": "Levi",
+ "hobbies": [
+ "Movie Watching",
+ "Church Activities",
+ "Shopping"
+ ]
+ }
+ ]
+ },
+ {
+ "id": 64,
+ "name": "Joe",
+ "city": "San Francisco",
+ "age": 37,
+ "friends": [
+ {
+ "name": "Michelle",
+ "hobbies": [
+ "Skiing & Snowboarding",
+ "Dancing"
+ ]
+ },
+ {
+ "name": "John",
+ "hobbies": [
+ "Running",
+ "Podcasts",
+ "Woodworking"
+ ]
+ }
+ ]
+ },
+ {
+ "id": 65,
+ "name": "Chloe",
+ "city": "Palm Springs",
+ "age": 60,
+ "friends": [
+ {
+ "name": "Lucas",
+ "hobbies": [
+ "Movie Watching",
+ "Walking"
+ ]
+ },
+ {
+ "name": "Grace",
+ "hobbies": [
+ "Volunteer Work",
+ "Socializing"
+ ]
+ },
+ {
+ "name": "Daniel",
+ "hobbies": [
+ "Church Activities",
+ "Gardening"
+ ]
+ },
+ {
+ "name": "Michael",
+ "hobbies": [
+ "Walking",
+ "Team Sports",
+ "Martial Arts"
+ ]
+ }
+ ]
+ },
+ {
+ "id": 66,
+ "name": "Leo",
+ "city": "New Orleans",
+ "age": 97,
+ "friends": [
+ {
+ "name": "Ava",
+ "hobbies": [
+ "Martial Arts",
+ "Woodworking",
+ "Quilting"
+ ]
+ },
+ {
+ "name": "Chloe",
+ "hobbies": [
+ "Fishing",
+ "Genealogy",
+ "Writing"
+ ]
+ },
+ {
+ "name": "Nora",
+ "hobbies": [
+ "Traveling",
+ "Woodworking"
+ ]
+ },
+ {
+ "name": "Victoria",
+ "hobbies": [
+ "Church Activities",
+ "Cooking"
+ ]
+ },
+ {
+ "name": "Camila",
+ "hobbies": [
+ "Video Games",
+ "Housework"
+ ]
+ }
+ ]
+ },
+ {
+ "id": 67,
+ "name": "Robert",
+ "city": "Austin",
+ "age": 19,
+ "friends": [
+ {
+ "name": "Joe",
+ "hobbies": [
+ "Writing",
+ "Yoga"
+ ]
+ },
+ {
+ "name": "Emma",
+ "hobbies": [
+ "Writing",
+ "Socializing"
+ ]
+ }
+ ]
+ },
+ {
+ "id": 68,
+ "name": "Robert",
+ "city": "Orlando",
+ "age": 65,
+ "friends": [
+ {
+ "name": "Mateo",
+ "hobbies": [
+ "Board Games",
+ "Cooking"
+ ]
+ },
+ {
+ "name": "Noah",
+ "hobbies": [
+ "Collecting",
+ "Housework",
+ "Skiing & Snowboarding"
+ ]
+ }
+ ]
+ },
+ {
+ "id": 69,
+ "name": "Mateo",
+ "city": "New Orleans",
+ "age": 95,
+ "friends": [
+ {
+ "name": "Chloe",
+ "hobbies": [
+ "Painting",
+ "Eating Out",
+ "Walking"
+ ]
+ },
+ {
+ "name": "Nora",
+ "hobbies": [
+ "Bicycling",
+ "Jewelry Making",
+ "Woodworking"
+ ]
+ },
+ {
+ "name": "Jack",
+ "hobbies": [
+ "Cooking",
+ "Gardening"
+ ]
+ },
+ {
+ "name": "Liam",
+ "hobbies": [
+ "Reading",
+ "Collecting",
+ "Podcasts"
+ ]
+ },
+ {
+ "name": "Noah",
+ "hobbies": [
+ "Housework",
+ "Team Sports"
+ ]
+ },
+ {
+ "name": "Emily",
+ "hobbies": [
+ "Dancing",
+ "Yoga"
+ ]
+ }
+ ]
+ },
+ {
+ "id": 70,
+ "name": "Jack",
+ "city": "Boston",
+ "age": 76,
+ "friends": [
+ {
+ "name": "Ava",
+ "hobbies": [
+ "Martial Arts",
+ "Volunteer Work",
+ "Team Sports"
+ ]
+ },
+ {
+ "name": "Kevin",
+ "hobbies": [
+ "Traveling",
+ "Bicycling"
+ ]
+ },
+ {
+ "name": "Lucas",
+ "hobbies": [
+ "Podcasts",
+ "Jewelry Making",
+ "Dancing"
+ ]
+ },
+ {
+ "name": "Michelle",
+ "hobbies": [
+ "Gardening",
+ "Shopping",
+ "Genealogy"
+ ]
+ },
+ {
+ "name": "Michelle",
+ "hobbies": [
+ "Writing",
+ "Tennis"
+ ]
+ }
+ ]
+ },
+ {
+ "id": 71,
+ "name": "Liam",
+ "city": "Savannah",
+ "age": 37,
+ "friends": [
+ {
+ "name": "Michelle",
+ "hobbies": [
+ "Painting",
+ "Volunteer Work"
+ ]
+ },
+ {
+ "name": "Kevin",
+ "hobbies": [
+ "Dancing",
+ "Fishing"
+ ]
+ },
+ {
+ "name": "Olivia",
+ "hobbies": [
+ "Television",
+ "Running"
+ ]
+ },
+ {
+ "name": "Robert",
+ "hobbies": [
+ "Fishing",
+ "Eating Out"
+ ]
+ },
+ {
+ "name": "John",
+ "hobbies": [
+ "Skiing & Snowboarding"
+ ]
+ },
+ {
+ "name": "Chloe",
+ "hobbies": [
+ "Church Activities",
+ "Calligraphy",
+ "Writing"
+ ]
+ }
+ ]
+ },
+ {
+ "id": 72,
+ "name": "Daniel",
+ "city": "Los Angeles",
+ "age": 63,
+ "friends": [
+ {
+ "name": "Evy",
+ "hobbies": [
+ "Television",
+ "Dancing"
+ ]
+ },
+ {
+ "name": "Kevin",
+ "hobbies": [
+ "Walking",
+ "Socializing",
+ "Writing"
+ ]
+ }
+ ]
+ },
+ {
+ "id": 73,
+ "name": "Olivia",
+ "city": "Boston",
+ "age": 89,
+ "friends": [
+ {
+ "name": "Ava",
+ "hobbies": [
+ "Fishing",
+ "Playing Cards"
+ ]
+ },
+ {
+ "name": "Noah",
+ "hobbies": [
+ "Movie Watching",
+ "Board Games"
+ ]
+ }
+ ]
+ },
+ {
+ "id": 74,
+ "name": "Amelia",
+ "city": "Orlando",
+ "age": 40,
+ "friends": [
+ {
+ "name": "Kevin",
+ "hobbies": [
+ "Golf",
+ "Reading",
+ "Shopping"
+ ]
+ },
+ {
+ "name": "Liam",
+ "hobbies": [
+ "Writing",
+ "Woodworking"
+ ]
+ },
+ {
+ "name": "Amelia",
+ "hobbies": [
+ "Movie Watching",
+ "Music"
+ ]
+ },
+ {
+ "name": "Grace",
+ "hobbies": [
+ "Jewelry Making",
+ "Bicycling"
+ ]
+ }
+ ]
+ },
+ {
+ "id": 75,
+ "name": "Camila",
+ "city": "New Orleans",
+ "age": 65,
+ "friends": [
+ {
+ "name": "Mateo",
+ "hobbies": [
+ "Yoga",
+ "Reading",
+ "Team Sports"
+ ]
+ },
+ {
+ "name": "Mateo",
+ "hobbies": [
+ "Board Games",
+ "Shopping"
+ ]
+ },
+ {
+ "name": "Leo",
+ "hobbies": [
+ "Woodworking",
+ "Martial Arts"
+ ]
+ },
+ {
+ "name": "Jack",
+ "hobbies": [
+ "Television",
+ "Calligraphy",
+ "Playing Cards"
+ ]
+ },
+ {
+ "name": "Liam",
+ "hobbies": [
+ "Fishing",
+ "Martial Arts"
+ ]
+ }
+ ]
+ },
+ {
+ "id": 76,
+ "name": "Jack",
+ "city": "Orlando",
+ "age": 42,
+ "friends": [
+ {
+ "name": "Daniel",
+ "hobbies": [
+ "Podcasts",
+ "Collecting"
+ ]
+ },
+ {
+ "name": "Robert",
+ "hobbies": [
+ "Running",
+ "Shopping",
+ "Quilting"
+ ]
+ },
+ {
+ "name": "Chris",
+ "hobbies": [
+ "Martial Arts",
+ "Golf",
+ "Quilting"
+ ]
+ },
+ {
+ "name": "Amelia",
+ "hobbies": [
+ "Eating Out",
+ "Bicycling",
+ "Golf"
+ ]
+ },
+ {
+ "name": "Olivia",
+ "hobbies": [
+ "Skiing & Snowboarding",
+ "Church Activities"
+ ]
+ }
+ ]
+ },
+ {
+ "id": 77,
+ "name": "Leo",
+ "city": "Lahaina",
+ "age": 46,
+ "friends": [
+ {
+ "name": "Robert",
+ "hobbies": [
+ "Traveling",
+ "Watching Sports"
+ ]
+ },
+ {
+ "name": "Daniel",
+ "hobbies": [
+ "Video Games",
+ "Music"
+ ]
+ },
+ {
+ "name": "Michael",
+ "hobbies": [
+ "Video Games",
+ "Gardening"
+ ]
+ },
+ {
+ "name": "Emma",
+ "hobbies": [
+ "Painting",
+ "Television"
+ ]
+ },
+ {
+ "name": "Sarah",
+ "hobbies": [
+ "Dancing",
+ "Tennis"
+ ]
+ }
+ ]
+ },
+ {
+ "id": 78,
+ "name": "Kevin",
+ "city": "San Antonio",
+ "age": 19,
+ "friends": [
+ {
+ "name": "Leo",
+ "hobbies": [
+ "Traveling",
+ "Television"
+ ]
+ },
+ {
+ "name": "Victoria",
+ "hobbies": [
+ "Fishing",
+ "Collecting",
+ "Gardening"
+ ]
+ },
+ {
+ "name": "Mia",
+ "hobbies": [
+ "Skiing & Snowboarding",
+ "Watching Sports",
+ "Martial Arts"
+ ]
+ }
+ ]
+ },
+ {
+ "id": 79,
+ "name": "Leo",
+ "city": "Sedona",
+ "age": 56,
+ "friends": [
+ {
+ "name": "Mateo",
+ "hobbies": [
+ "Board Games",
+ "Reading"
+ ]
+ },
+ {
+ "name": "Grace",
+ "hobbies": [
+ "Reading",
+ "Fishing",
+ "Woodworking"
+ ]
+ },
+ {
+ "name": "Mia",
+ "hobbies": [
+ "Gardening",
+ "Woodworking"
+ ]
+ },
+ {
+ "name": "Noah",
+ "hobbies": [
+ "Video Games",
+ "Television",
+ "Eating Out"
+ ]
+ }
+ ]
+ },
+ {
+ "id": 80,
+ "name": "Charlotte",
+ "city": "Orlando",
+ "age": 73,
+ "friends": [
+ {
+ "name": "Camila",
+ "hobbies": [
+ "Golf",
+ "Collecting"
+ ]
+ },
+ {
+ "name": "Evy",
+ "hobbies": [
+ "Shopping",
+ "Yoga",
+ "Genealogy"
+ ]
+ },
+ {
+ "name": "Charlotte",
+ "hobbies": [
+ "Yoga",
+ "Volunteer Work"
+ ]
+ }
+ ]
+ },
+ {
+ "id": 81,
+ "name": "Robert",
+ "city": "Chicago",
+ "age": 52,
+ "friends": [
+ {
+ "name": "Noah",
+ "hobbies": [
+ "Church Activities",
+ "Woodworking",
+ "Traveling"
+ ]
+ },
+ {
+ "name": "Liam",
+ "hobbies": [
+ "Board Games",
+ "Socializing"
+ ]
+ },
+ {
+ "name": "Liam",
+ "hobbies": [
+ "Housework",
+ "Music",
+ "Calligraphy"
+ ]
+ },
+ {
+ "name": "Zoey",
+ "hobbies": [
+ "Shopping",
+ "Fishing",
+ "Walking"
+ ]
+ },
+ {
+ "name": "Grace",
+ "hobbies": [
+ "Dancing",
+ "Yoga"
+ ]
+ }
+ ]
+ },
+ {
+ "id": 82,
+ "name": "Kevin",
+ "city": "Palm Springs",
+ "age": 75,
+ "friends": [
+ {
+ "name": "Oliver",
+ "hobbies": [
+ "Running",
+ "Traveling"
+ ]
+ },
+ {
+ "name": "Luke",
+ "hobbies": [
+ "Socializing",
+ "Martial Arts",
+ "Running"
+ ]
+ }
+ ]
+ },
+ {
+ "id": 83,
+ "name": "Evy",
+ "city": "Palm Springs",
+ "age": 51,
+ "friends": [
+ {
+ "name": "Michael",
+ "hobbies": [
+ "Writing",
+ "Podcasts"
+ ]
+ },
+ {
+ "name": "Noah",
+ "hobbies": [
+ "Yoga",
+ "Quilting",
+ "Fishing"
+ ]
+ },
+ {
+ "name": "Sophie",
+ "hobbies": [
+ "Painting"
+ ]
+ },
+ {
+ "name": "Olivia",
+ "hobbies": [
+ "Martial Arts",
+ "Shopping",
+ "Podcasts"
+ ]
+ },
+ {
+ "name": "Camila",
+ "hobbies": [
+ "Reading",
+ "Collecting"
+ ]
+ },
+ {
+ "name": "Noah",
+ "hobbies": [
+ "Socializing",
+ "Housework"
+ ]
+ }
+ ]
+ },
+ {
+ "id": 84,
+ "name": "Daniel",
+ "city": "Saint Augustine",
+ "age": 57,
+ "friends": [
+ {
+ "name": "Emily",
+ "hobbies": [
+ "Walking",
+ "Painting",
+ "Reading"
+ ]
+ },
+ {
+ "name": "Oliver",
+ "hobbies": [
+ "Team Sports",
+ "Board Games"
+ ]
+ },
+ {
+ "name": "Levi",
+ "hobbies": [
+ "Jewelry Making",
+ "Eating Out",
+ "Volunteer Work"
+ ]
+ },
+ {
+ "name": "Zoey",
+ "hobbies": [
+ "Movie Watching",
+ "Video Games"
+ ]
+ },
+ {
+ "name": "Sophie",
+ "hobbies": [
+ "Watching Sports",
+ "Walking",
+ "Martial Arts"
+ ]
+ }
+ ]
+ },
+ {
+ "id": 85,
+ "name": "Olivia",
+ "city": "Charleston",
+ "age": 63,
+ "friends": [
+ {
+ "name": "Oliver",
+ "hobbies": [
+ "Reading",
+ "Playing Cards"
+ ]
+ },
+ {
+ "name": "Mia",
+ "hobbies": [
+ "Running",
+ "Shopping"
+ ]
+ },
+ {
+ "name": "John",
+ "hobbies": [
+ "Writing",
+ "Walking",
+ "Tennis"
+ ]
+ }
+ ]
+ },
+ {
+ "id": 86,
+ "name": "Amelia",
+ "city": "Seattle",
+ "age": 96,
+ "friends": [
+ {
+ "name": "Daniel",
+ "hobbies": [
+ "Dancing",
+ "Eating Out"
+ ]
+ },
+ {
+ "name": "Levi",
+ "hobbies": [
+ "Bicycling",
+ "Dancing"
+ ]
+ },
+ {
+ "name": "Daniel",
+ "hobbies": [
+ "Writing",
+ "Shopping",
+ "Tennis"
+ ]
+ },
+ {
+ "name": "Michelle",
+ "hobbies": [
+ "Board Games",
+ "Walking",
+ "Housework"
+ ]
+ },
+ {
+ "name": "Chloe",
+ "hobbies": [
+ "Genealogy",
+ "Dancing",
+ "Podcasts"
+ ]
+ },
+ {
+ "name": "Joe",
+ "hobbies": [
+ "Movie Watching",
+ "Cooking",
+ "Housework"
+ ]
+ }
+ ]
+ },
+ {
+ "id": 87,
+ "name": "Luke",
+ "city": "Seattle",
+ "age": 26,
+ "friends": [
+ {
+ "name": "Isabella",
+ "hobbies": [
+ "Traveling",
+ "Walking",
+ "Team Sports"
+ ]
+ },
+ {
+ "name": "Amelia",
+ "hobbies": [
+ "Writing",
+ "Housework",
+ "Volunteer Work"
+ ]
+ },
+ {
+ "name": "Elijah",
+ "hobbies": [
+ "Golf",
+ "Yoga"
+ ]
+ },
+ {
+ "name": "Levi",
+ "hobbies": [
+ "Volunteer Work",
+ "Eating Out"
+ ]
+ },
+ {
+ "name": "Kevin",
+ "hobbies": [
+ "Yoga",
+ "Genealogy",
+ "Volunteer Work"
+ ]
+ },
+ {
+ "name": "Levi",
+ "hobbies": [
+ "Tennis",
+ "Television"
+ ]
+ }
+ ]
+ },
+ {
+ "id": 88,
+ "name": "Chris",
+ "city": "Nashville",
+ "age": 34,
+ "friends": [
+ {
+ "name": "Kevin",
+ "hobbies": [
+ "Podcasts",
+ "Team Sports",
+ "Traveling"
+ ]
+ },
+ {
+ "name": "Elijah",
+ "hobbies": [
+ "Television",
+ "Woodworking",
+ "Cooking"
+ ]
+ },
+ {
+ "name": "Evy",
+ "hobbies": [
+ "Podcasts",
+ "Genealogy",
+ "Calligraphy"
+ ]
+ },
+ {
+ "name": "Victoria",
+ "hobbies": [
+ "Fishing",
+ "Church Activities",
+ "Collecting"
+ ]
+ },
+ {
+ "name": "Camila",
+ "hobbies": [
+ "Television",
+ "Writing"
+ ]
+ },
+ {
+ "name": "Michelle",
+ "hobbies": [
+ "Yoga",
+ "Running"
+ ]
+ }
+ ]
+ },
+ {
+ "id": 89,
+ "name": "Michelle",
+ "city": "Honolulu",
+ "age": 85,
+ "friends": [
+ {
+ "name": "Isabella",
+ "hobbies": [
+ "Calligraphy",
+ "Gardening"
+ ]
+ },
+ {
+ "name": "Chloe",
+ "hobbies": [
+ "Shopping",
+ "Playing Cards",
+ "Tennis"
+ ]
+ },
+ {
+ "name": "Evy",
+ "hobbies": [
+ "Watching Sports",
+ "Cooking",
+ "Golf"
+ ]
+ },
+ {
+ "name": "Elijah",
+ "hobbies": [
+ "Writing",
+ "Tennis",
+ "Playing Cards"
+ ]
+ }
+ ]
+ },
+ {
+ "id": 90,
+ "name": "Lucas",
+ "city": "Los Angeles",
+ "age": 78,
+ "friends": [
+ {
+ "name": "Emma",
+ "hobbies": [
+ "Woodworking",
+ "Painting",
+ "Television"
+ ]
+ },
+ {
+ "name": "Lucas",
+ "hobbies": [
+ "Bicycling",
+ "Volunteer Work"
+ ]
+ },
+ {
+ "name": "Grace",
+ "hobbies": [
+ "Dancing",
+ "Running"
+ ]
+ }
+ ]
+ },
+ {
+ "id": 91,
+ "name": "Sophie",
+ "city": "St. Louis",
+ "age": 86,
+ "friends": [
+ {
+ "name": "Joe",
+ "hobbies": [
+ "Socializing",
+ "Music"
+ ]
+ },
+ {
+ "name": "Zoey",
+ "hobbies": [
+ "Running",
+ "Playing Cards"
+ ]
+ },
+ {
+ "name": "Elijah",
+ "hobbies": [
+ "Dancing"
+ ]
+ }
+ ]
+ },
+ {
+ "id": 92,
+ "name": "Victoria",
+ "city": "Saint Augustine",
+ "age": 33,
+ "friends": [
+ {
+ "name": "Leo",
+ "hobbies": [
+ "Socializing",
+ "Fishing"
+ ]
+ },
+ {
+ "name": "Emily",
+ "hobbies": [
+ "Video Games",
+ "Watching Sports"
+ ]
+ },
+ {
+ "name": "Luke",
+ "hobbies": [
+ "Martial Arts"
+ ]
+ },
+ {
+ "name": "Oliver",
+ "hobbies": [
+ "Traveling",
+ "Quilting",
+ "Television"
+ ]
+ },
+ {
+ "name": "Charlotte",
+ "hobbies": [
+ "Gardening",
+ "Cooking",
+ "Housework"
+ ]
+ }
+ ]
+ },
+ {
+ "id": 93,
+ "name": "Michael",
+ "city": "New Orleans",
+ "age": 82,
+ "friends": [
+ {
+ "name": "Jack",
+ "hobbies": [
+ "Bicycling",
+ "Board Games",
+ "Movie Watching"
+ ]
+ },
+ {
+ "name": "Liam",
+ "hobbies": [
+ "Painting",
+ "Writing",
+ "Bicycling"
+ ]
+ }
+ ]
+ },
+ {
+ "id": 94,
+ "name": "Michael",
+ "city": "Seattle",
+ "age": 49,
+ "friends": [
+ {
+ "name": "John",
+ "hobbies": [
+ "Collecting",
+ "Playing Cards",
+ "Cooking"
+ ]
+ },
+ {
+ "name": "Sarah",
+ "hobbies": [
+ "Fishing",
+ "Walking",
+ "Movie Watching"
+ ]
+ }
+ ]
+ },
+ {
+ "id": 95,
+ "name": "Victoria",
+ "city": "Branson",
+ "age": 48,
+ "friends": [
+ {
+ "name": "Amelia",
+ "hobbies": [
+ "Painting",
+ "Volunteer Work",
+ "Socializing"
+ ]
+ },
+ {
+ "name": "Evy",
+ "hobbies": [
+ "Skiing & Snowboarding",
+ "Volunteer Work"
+ ]
+ },
+ {
+ "name": "Amelia",
+ "hobbies": [
+ "Genealogy",
+ "Reading",
+ "Yoga"
+ ]
+ },
+ {
+ "name": "Sophie",
+ "hobbies": [
+ "Movie Watching",
+ "Golf",
+ "Television"
+ ]
+ },
+ {
+ "name": "Charlotte",
+ "hobbies": [
+ "Jewelry Making",
+ "Quilting",
+ "Playing Cards"
+ ]
+ },
+ {
+ "name": "Jack",
+ "hobbies": [
+ "Playing Cards",
+ "Golf"
+ ]
+ }
+ ]
+ },
+ {
+ "id": 96,
+ "name": "Grace",
+ "city": "Seattle",
+ "age": 89,
+ "friends": [
+ {
+ "name": "Chris",
+ "hobbies": [
+ "Board Games",
+ "Golf",
+ "Playing Cards"
+ ]
+ },
+ {
+ "name": "Emily",
+ "hobbies": [
+ "Video Games",
+ "Golf"
+ ]
+ },
+ {
+ "name": "Victoria",
+ "hobbies": [
+ "Housework",
+ "Collecting",
+ "Woodworking"
+ ]
+ }
+ ]
+ },
+ {
+ "id": 97,
+ "name": "Liam",
+ "city": "Nashville",
+ "age": 64,
+ "friends": [
+ {
+ "name": "Kevin",
+ "hobbies": [
+ "Collecting"
+ ]
+ },
+ {
+ "name": "Amelia",
+ "hobbies": [
+ "Golf",
+ "Playing Cards",
+ "Cooking"
+ ]
+ },
+ {
+ "name": "Charlotte",
+ "hobbies": [
+ "Reading",
+ "Board Games",
+ "Genealogy"
+ ]
+ },
+ {
+ "name": "Leo",
+ "hobbies": [
+ "Video Games",
+ "Writing"
+ ]
+ },
+ {
+ "name": "Nora",
+ "hobbies": [
+ "Jewelry Making",
+ "Volunteer Work"
+ ]
+ }
+ ]
+ },
+ {
+ "id": 98,
+ "name": "Mia",
+ "city": "Miami Beach",
+ "age": 77,
+ "friends": [
+ {
+ "name": "Emma",
+ "hobbies": [
+ "Podcasts",
+ "Movie Watching"
+ ]
+ },
+ {
+ "name": "Oliver",
+ "hobbies": [
+ "Playing Cards",
+ "Fishing",
+ "Eating Out"
+ ]
+ },
+ {
+ "name": "Emma",
+ "hobbies": [
+ "Collecting",
+ "Yoga"
+ ]
+ },
+ {
+ "name": "Michael",
+ "hobbies": [
+ "Bicycling",
+ "Team Sports"
+ ]
+ },
+ {
+ "name": "Ava",
+ "hobbies": [
+ "Watching Sports",
+ "Jewelry Making"
+ ]
+ },
+ {
+ "name": "Joe",
+ "hobbies": [
+ "Video Games",
+ "Woodworking",
+ "Music"
+ ]
+ }
+ ]
+ },
+ {
+ "id": 99,
+ "name": "Mateo",
+ "city": "Branson",
+ "age": 66,
+ "friends": [
+ {
+ "name": "Isabella",
+ "hobbies": [
+ "Television",
+ "Skiing & Snowboarding"
+ ]
+ },
+ {
+ "name": "Noah",
+ "hobbies": [
+ "Housework",
+ "Running",
+ "Podcasts"
+ ]
+ }
+ ]
+ }
+ ]
+}
\ No newline at end of file