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:
|
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
(includingboolean
), andCollection
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 theJSON
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).
|