JSON library (Java)
Introduction
This is Nayuki’s library for parsing and serializing JSON data, open-source and written in Java. It is compact at roughly 1000 lines of code in 2 source files, and provides a small, easy-to-use API.
I created this library because while browsing the web, I couldn’t find a Java JSON library that suited my needs. The libraries I encountered had problems like large code bases (e.g. 3000 lines of code in 10 files), multiple dependencies, wrapper objects around every piece of data, Java reflection magic, etc.
This library converts between standard Java objects (Integer, Double, String, List, Map, some others) and JSON data as Unicode strings. The overall concept was inspired by the straightforwardness of Python’s built-in json
library.
My JSON library provides two static methods serialize()
and parse()
, a set of path-based accessor methods like getInt()
, one custom data structure JsonNumber
, and a few convenience I/O methods to read from file/URL or write to file. It does not provide any other wrappers, factories, or configuration. It only has a dependency on Java SE classes.
Download
Full package: nayuki-json-lib.jar (source code, compiled classes, Javadoc HTML)
Individual source files:
- Json.java (main logic)
- JsonNumber.java (supporting data structure)
- JsonTest.java (JUnit test suite)
Examples
import io.nayuki.json.Json; import io.nayuki.json.JsonNumber; /* Parsing and queries */ Object dec = Json.parse("[99, 88, 77]"); int a = Json.getInt (dec, 0); // 99 long b = Json.getLong (dec, 1); // 88L double c = Json.getDouble(dec, 2); // 77.0 Object dec = Json.parse( "{\"a\":\"alpha\", \"b\":[\"beta\",\"bravo\",\"buck\"]}"); String a = Json.getString(dec, "a"); // alpha String b0 = Json.getString(dec, "b", 0); // beta String b1 = Json.getString(dec, "b", 1); // bravo String b2 = Json.getString(dec, "b", 2); // buck /* Numbers */ Object orig = 1234; // Integer String enc = Json.serialize(orig); // 1234 Object dec = Json.parse(enc); // JsonNumber int val = Json.getInt(dec); // 1234 int val = ((Number)dec).intValue(); // 1234 Object orig = 3.1415; // Double String enc = Json.serialize(orig); // 3.1415 (due to Double.toString()) Object dec = Json.parse(enc); // JsonNumber double val = Json.getDouble(dec); // 3.1415 double val = ((Number)dec).doubleValue(); // 3.1415 Object orig = Double.NaN; // Double String enc = Json.serialize(orig); // IllegalArgumentException: Cannot encode Object orig = BigInteger.ONE.shiftLeft(192); // 2^192 String enc = Json.serialize(orig); // enc == 6277101735386680763835789423207666416102355444464034512896 Object dec = Json.parse(enc); // JsonNumber BigInteger val = ((JsonNumber)dec).bigIntegerValue(); // 6277...2896 (exact) double val = ((Number)dec).doubleValue(); // 6.277101735386681e57 long val = ((Number)dec).longValue(); // NumberFormatException: Overflow /* Strings */ Object orig = "hello"; // String String enc = Json.serialize(orig); // "hello" (output has quotes) Object dec = Json.parse(enc); // String String val = Json.getString(dec); // hello String val = (String)dec; // hello Object orig = "你好" + (char)10; // String String enc = Json.serialize(orig); // "\u4f60\u597d\n" (with quotes) Object dec = Json.parse(enc); // String String val = Json.getString(dec); // 你好\n String val = (String)dec; // 你好\n /* Lists and maps */ List<Object> orig = new ArrayList<>(); String enc = Json.serialize(orig); // [] orig.add(4); orig.add("N"); orig.add(new ArrayList<Object>()); String enc = Json.serialize(orig); // [4, "N", []] Object dec = Json.parse(enc); // List<Object> List<Object> val = Json.getList(dec); List<Object> val = (List<Object>)dec; Map<String,Object> orig = new TreeMap<>(); orig.put("a", 7); orig.put("b", 9); orig.put("c", 6); orig.put("d", 8); String enc = Json.serialize(orig); // {"a":7, "b":9, "c":6, "d":8} Object dec = Json.parse(enc); // SortedMap<String,Object> Map<String,Object> val = Json.getMap(dec); Map<String,Object> val = (Map<String,Object>)dec;
Documentation
class Json
in package io.nayuki.json
Provides static methods to convert between Java objects and JSON text, and also to query JSON data structures.
JSON | Java |
---|---|
null | null |
false | java.lang.Boolean.FALSE |
true | java.lang.Boolean.TRUE |
0, 1.2, -3.4e+5 | java.lang.Number / io.nayuki.json.JsonNumber |
"abc" | java.lang.String / java.lang.CharSequence |
[...] | java.util.List<Object> |
{...} | java.util.Map<String,Object> |
See the full details and caveats in serialize()
and parse()
.
public static String serialize(Object obj)
Serializes the specified Java object / tree of objects into a JSON text string.
There are a number of restrictions on the input object/tree:
- Every object in the tree must be either
null
,Boolean
,Number
,String
/CharSequence
,List
, orMap
. All other types are illegal. Double
/Float
values must be finite, not infinity or NaN.- User-defined
Number
objects must produce strings that satisfy the JSON number syntax (e.g. a fraction string like"1/2"
is disallowed). - No object can implement more than one of these interfaces:
CharSequence
,List
,Map
.
Note that all Unicode strings can be encoded to JSON. This includes things like embedded nulls (U+0000), as well as characters outside the Basic Multilingual Plane (over U+FFFF).
The returned string is pure ASCII with no control characters, i.e. all characters are in the range [0x20, 0x7E]. This means that all ASCII control characters and above-ASCII Unicode characters are escaped, and there are no tabs or line breaks in the output string.
- Parameters:
-
- obj – the object/tree to serialize to JSON (can be
null
)
- obj – the object/tree to serialize to JSON (can be
- Returns:
-
- a JSON text string representing the given object/tree (not
null
)
- a JSON text string representing the given object/tree (not
- Throws:
-
- IllegalArgumentException – if any of the restrictions are violated
public static Object parse(String str)
Parses the specified JSON text into a Java object / tree of objects. Notes:
- The user is responsible for performing
instanceof
tests and class casting, because most methods return a genericObject
. - All numbers are parsed into
JsonNumber
objects. The user needs to callintValue()
,doubleValue()
, etc. as appropriate. - All JSON objects (
{...}
) are parsed into JavaSortedMap<String,Object>
objects. Although technically allowed by the JSON specification, the input JSON object must not have duplicate string keys for simplicity and compatibility with Java maps. The sorted map allows the user to iterate over keys in ascending order. Furthermore, the map’sget()
method is customized so that if a given key is not found, it will throw anIllegalArgumentException
– this differs from how standard Java map implementations returnnull
in this situation. This safety feature prevents the user from confusing a non-existent key from a key that is truly mapped to anull
value, both of which are expressible distinctly in JSON. - The JSON text must have exactly one root object and no data afterward except whitespace. For example, these JSON strings are considered invalid:
"[0,0] []"
,"1 2 3"
.
- Parameters:
-
- str – the JSON text string (not
null
)
- str – the JSON text string (not
- Returns:
-
- the object/tree (can be
null
) corresponding to the JSON data
- the object/tree (can be
- Throws:
-
- IllegalArgumentException – if the JSON text fails to conform to the standard syntax in any manner or an object has a duplicate key
public static Object getObject(Object root, Object... path)
Traverses the specified JSON object/tree along the specified path through maps and lists, and returns the object at that location. The path is a (possibly empty) sequence of strings or integers.
(The related methods getInt()
, getLong()
, getFloat()
, getDouble()
, getString()
, getList()
, getMap()
behave similarly.)
For example, in this data structure:
data = { "alpha": null, "beta" : [9, 88, 777], "gamma": ["x", 3.21, { "y": 5, "z": 6 }] }
The following queries produce these results:
getObject(data): Map[alpha=null, beta=List[9, 88, 777],
gamma=List["x", 3.21, Map[y=5, z=6]]]
getObject(data, "alpha"): null
getObject(data, "beta"): List[9, 88, 777]
getObject(data, "beta", 0): 9
getObject(data, "beta", 1): 88
getObject(data, "beta", 2): 777
getObject(data, "beta", 3): IndexOutOfBoundsException
getObject(data, "charlie"): IllegalArgumentException (no such key)
getObject(data, "gamma"): List["x", 3.21, Map[y=5, z=6]]
getObject(data, "gamma", 0): "x"
getObject(data, "gamma", 1): 3.21
getObject(data, "gamma", 2): Map[y=5, z=6]
getObject(data, "gamma", 2, "y"): 5
getObject(data, "gamma", 2, "z"): 6
getObject(data, "gamma", "2"): IllegalArgumentException (map expected)
getObject(data, 0): IllegalArgumentException (list expected)
- Parameters:
-
- root – the JSON object/tree to query
- path – the sequence of strings and integers that expresses the query path
- Returns:
-
- the object at the location (can be
null
)
- the object at the location (can be
- Throws:
-
- IllegalArgumentException – if a map/list was expected but not found, or a map key was not found, or a path component is not a string/integer
- IndexOutOfBoundsException – if a list index is negative or greater/equal to the list length
- NullPointerException – if any argument or path component is
null
(except if the root isnull
and the path is zero-length
public static Object parseFromFile(File file)
public static Object parseFromUrl(URL url)
public static void serializeToFile(Object obj, File file)
These are convenience methods for parse(String)
and serialize(Object)
, and their behavior should be self-explanatory.
Notes
The JSON data format allows arbitrary-precision numbers in decimal / scientific notation. My
JsonNumber
class exists to preserve this precision, and it is the user’s responsibility to callintValue()
,doubleValue()
, etc. to parse the number string at the desired output precision. This perhaps differs from the design of other JSON libraries, which parse the number immediately at some predetermined precision (such as double or long).-
When parsing JSON text, this library discards exactly these pieces of information:
Whitespace outside of strings (e.g. the JSON text [2,3] and [ 2 , 3 ] both decode to the same tree of objects)
String escape sequences (e.g. "a" and "\u0061" both decode to the same string object)
Object key ordering (e.g. {"a":0, "b":1} and {"b":1, "a":0} both decode to the same map objects)
Furthermore, there is the restriction that duplicate object keys are not allowed (even though the JSON specification allows it).
My JSON parser implementation is a hand-written character tokenizer driven by a recursive-descent parser. It can cause a stack overflow if the data structure is deeply nested (like nesting objects/arrays 1000 levels deep). Similarly, the serializer can suffer from stack overflow because it recurses on the data structure. Both of these problems can be solved with an on-heap stack plus extra, uglier code logic, but a conscious decision was made to not support these unlikely use-cases on deeply nested or malicious data structures.
My JSON library does not support parsing non-conforming JSON variant syntaxes such as: Single-quoted strings, JavaScript comments (
//
and/**/
), unquoted object keys, trailing comma in array/object, infinity/NaN numbers. This choice improves the simplicity, testability, and reliability of the library.