Project Nayuki


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:


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.

JSONJava
nullnull
falsejava.lang.Boolean.FALSE
truejava.lang.Boolean.TRUE
0, 1.2, -3.4e+5java.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, or Map. 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)
Returns:
  • a JSON text string representing the given object/tree (not null)
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 generic Object.
  • All numbers are parsed into JsonNumber objects. The user needs to call intValue(), doubleValue(), etc. as appropriate.
  • All JSON objects ({...}) are parsed into Java SortedMap<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’s get() method is customized so that if a given key is not found, it will throw an IllegalArgumentException – this differs from how standard Java map implementations return null in this situation. This safety feature prevents the user from confusing a non-existent key from a key that is truly mapped to a null 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)
Returns:
  • the object/tree (can be null) corresponding to the JSON data
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)
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 is null 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 call intValue(), 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.