Jetty JSON Support

JSON is a wide-spread format for data exchange.

Jetty offers a JSON library to parse JSON into objects, both synchronously and asynchronously, and to generate JSON from objects.

The Maven coordinates for the Jetty JSON support are:

<dependency>
  <groupId>org.eclipse.jetty</groupId>
  <artifactId>jetty-util-ajax</artifactId>
  <version>12.1.2-SNAPSHOT</version>
</dependency>

JSON Parsing

JSON parsing is available in two flavors:

  • Synchronous parsing, where the JSON document is fully available

  • Asynchronous parsing, where the JSON document is not fully available, and it is provided in chunks to the parser.

You can use asynchronous parsing also if the JSON document is fully available (there will be just one chunk representing the whole JSON document). You cannot use synchronous parsing without the full JSON document (the synchronous parser will produce an error, complaining that the JSON document is incomplete).

For synchronous parsing, you can use the org.eclipse.jetty.util.ajax.JSON class:

// Full JSON document.
String json = """
    {
      "field": "value",
      "count": 42,
      "array": ["one", "two"],
      "object": {
        "size": 13
      }
    }
    """;
JSON parser = new JSON();
Object object = parser.parse(new JSON.StringSource(json));
// The object can be cast to a Map, given the JSON document structure.
Map<String, Object> map = (Map<String, Object>)object;

For asynchronous parsing, you can use org.eclipse.jetty.util.ajax.AsyncJSON:

AsyncJSON parser = new AsyncJSON.Factory().newAsyncJSON();

// Simulate a read from the network of a
// first, partial, sequence of JSON bytes.
String jsonChunk1 = """
    {
      "field": "value",
      "cou""";
ByteBuffer byteBuffer1 = StandardCharsets.UTF_8.encode(jsonChunk1);
parser.parse(byteBuffer1);

// Simulate a read from the network of a second
// sequence of JSON bytes that completes the first read.
String jsonChunk2 = """
    nt": 42,
      "array": ["one", "two"],
      "object": {
        "size": 13
      }
    }
    """;
ByteBuffer byteBuffer2 = StandardCharsets.UTF_8.encode(jsonChunk2);
parser.parse(byteBuffer2);

// When all the JSON chunks are parsed, complete the parser.
Object object = parser.complete();
// The object can be cast to a Map, given the JSON document structure.
Map<String, Object> map = (Map<String, Object>)object;

By default, both parsers produce a Map<String, Object> for JSON objects.

By default, the JSON parser produces Object[] for JSON arrays, while the AsyncJSON parser produces List<Object> for JSON arrays. For both JSON and AsyncJSON.Factory you can call setArrayConverter(Function<List<?>, Object>) to convert JSON arrays, provided as List<Object> by the implementation, into the preferred data structure, typically either a Java array or a Java list.

By default, numbers are Long when they are integer (without a fractional part), Double when they are non-integer (with a fractional part).

Custom Objects Parsing

The Jetty JSON library supports converting JSON object into custom objects, using the class field (or the x-class field, in case class cannot be used).

The Java class specified in the class or x-class field must either implement JSON.Convertible or be mapped to a JSON.Convertor in the JSON instance or the AsyncJSON.Factory instance. For example:

String json = """
    {
      "x-class": "com.acme.Person",
      "firstName": "John",
      "lastName": "Doe",
      "age": 42
      "phone": {
        "x-class": "com.acme.PhoneNumber",
        "number": "+1 800 123456"
      },
      "address": {
        "x-class": "com.acme.Address",
        "address": "123 Main Street"
      }
    }
    """;

// This class depends on the Jetty JSON library
// by implementing JSON.Convertible.
class PhoneNumber implements JSON.Convertible
{
    private Parts parts;

    @Override
    public void fromJSON(Map<String, Object> object)
    {
        String number = (String)object.get("number");
        parts = parsePhoneNumber(number);
    }

    @Override
    public void toJSON(JSON.Output out)
    {
        out.add("x-class", PhoneNumber.class.getName());
        out.add("number", parts.asString());
    }

    // Split into Country Code, National Destination Code, and Subscriber Number.
    public record Parts(int cc, int ndc, int sn)
    {
        public String asString()
        {
            return "+%d %d %d".formatted(cc, ndc, sn);
        }
    }
}

// This class is independent of the Jetty JSON library,
// and relies on a JSON.Convertor configured externally.
class Address
{
    private final String address;

    public Address(String address)
    {
        this.address = address;
    }

    public String getAddress()
    {
        return address;
    }
}

// This is the convertor for the Address class,
// configured on the JSON or AsyncJSON.Factory instances.
class AddressConvertor implements JSON.Convertor
{
    @Override
    public Object fromJSON(Map<String, Object> object)
    {
        String address = (String)object.get("address");
        return new Address(address);
    }

    @Override
    public void toJSON(Object object, JSON.Output out)
    {
        Address address = (Address)object;
        out.add("x-class", Address.class.getName());
        out.add("address", address.getAddress());
    }
}

// This record is independent of the Jetty JSON library
// but records do not need external configuration for
// conversion to/from the JSON format.
record Person(String firstName, String lastName, int age, PhoneNumber phone)
{
}

JSON parser = new JSON();
// Configure the convertor for the Address class.
parser.addConvertor(Address.class, new AddressConvertor());

Person person = (Person)parser.parse(new JSON.StringSource(json));
Unlike other popular JSON libraries, the Jetty JSON library does not use Java annotations to map Java object and fields to JSON objects and fields.

The Jetty JSON library provides useful convertors out of the box:

  • JSONCollectionConvertor that converts Collection instances as JSON objects, so that the specific Collection implementation (if it is a public class) can be restored. This is useful when the Collection is a HashSet or ArrayDeque, or when it needs to be a specific List implementation such as LinkedList.

  • JSONDateConvertor, that converts from/to java.util.Date.

  • JSONEnumConvertor, that converts from/to enum constants.

  • JSONPojoConvertor, that converts from/to plain old Java objects (POJO) using reflection to invoke getters and setters.

JSON Generation

JSON generation is available via the org.eclipse.jetty.util.ajax.JSON class.

Simple Map<String, Object> objects can be directly generated:

JSON generator = new JSON();

Map<String, Object> object = new HashMap<>();
object.put("user", "John Doe");
object.put("score", 1234);
object.put("friends", List.of("Mary Major", "Richard Roe"));

String json = generator.toJSON(object);

// The object above is converted to this JSON document.
json = """
    {
      "user": "John Doe",
      "score": 1234,
      "friends": ["Mary Major", "Richard Roe"]
    }
    """;
The JSON object used as the parser and as the generator is typically just one instance, used for both parsing and generation. This also centralizes the configuration of convertors.

Custom Objects Generation

Similarly to the custom objects parsing, generation of custom objects into the JSON format relies on:

  • The object is composed (recursively) only of simple JSON types: Map<String, Object>, String, Number (including primitive number types), Boolean (including boolean), and Collection or arrays of these simple types.

  • The object being an instance of a Java record.

  • The class of the object implementing JSON.Convertible.

  • A JSON.Convertor configured in the JSON instance for the class of the object.

Generic objects that do not match the requisites above are converted to strings by calling toString(), and therefore the object structure will be lost (and the object cannot be reconstructed during JSON parsing).