Java does not have a JSON library built into the core library. That's not a problem because you can choose from many different 3rd party JSON libraries. Commonly used libraries are
Jackson and Gson.
In this blog post, we are going to look at JSON-P (Java API for JSON Processing, JSR 374) Oracle's attempt
to establish a standard JSON interface.
JSR 374 specifies a very low-level processing library. This specification does not describe a data binding between JSON and Java objects. For that, a second specification was created JSON Binding (JSON-B, JSR 367), which is based on JSR 374.
Like a lot of JSRs, JSR 374 only describes the interface which libraries have to implement. For the following examples, we are using the Glassfish reference implementation.
<groupId>org.glassfish</groupId>
<artifactId>jakarta.json</artifactId>
<version>2.0.1</version>
</dependency>
</dependencies>
JSR 374 provides two models for JSON processing. The Object Model API and the Streaming API. The object model API provides an easy way to work with JSON and holds all the data in memory. The streaming API is a low-level API designed to process large amounts of JSON data efficiently.
The main entry point to the API is the class Json. This class provides static methods to create parsers, readers, generators, and writers.
Object Model API ¶
This is an API that provides immutable object models for JSON object and array structures.
The API uses builder patterns to create these object models
The API uses JsonReader
to read JSON from an input source and JsonWriter
to write it into an output source.
Write ¶
To create a JSON with the Object Model API, you need an instance of the JsonObjectBuilder
which you
create with the static method Json.createObjectBuilder()
.
The JsonObjectBuilder
instance is at first empty, and you need to call add()
to add key/value pairs. The library provides add()
variants for all supported JSON datatypes.
The special addNull()
method inserts a property with the value null
.
To create an array you need a JsonArrayBuilder
instance which the static method Json.createArrayBuilder()
returns.
The array builder provides add()
methods to add the individual entries of the array.
JsonObject model = Json.createObjectBuilder().add("id", 1234).add("active", true)
.add("name", "Duke").addNull("password")
.add("roles", Json.createArrayBuilder().add("admin").add("user").add("operator"))
.add("phoneNumbers",
Json.createArrayBuilder()
.add(Json.createObjectBuilder().add("type", "mobile").add("number",
"111-111-1111"))
.add(Json.createObjectBuilder().add("type", "home").add("number",
"222-222-2222")))
.build();
The whole JSON will be created in memory and can be written with a JsonWriter
to an output stream.
You create the writer with the static method Json.createWriter
. By default the writer
prints the JSON in a compact form in one line
try (JsonWriter defaultWriter = Json.createWriter(System.out)) {
defaultWriter.write(model);
}
// {"id":1234,"active":true,"name":"Duke","password":null,"roles":["admin","user","operator"],"phoneNumbers":[{"type":"mobile","number":"111-111-1111"},{"type":"home","number":"222-222-2222"}]}
If you require a readable output, you create the writer with the PRETTY_PRINTING
flag set to on.
Map<String, Boolean> properties = Map.of(JsonGenerator.PRETTY_PRINTING, Boolean.TRUE);
try (JsonWriter customWriter = Json.createWriterFactory(properties)
.createWriter(System.out)) {
customWriter.write(model);
}
{
"id": 1234,
"active": true,
"name": "Duke",
"password": null,
"roles": [
"admin",
"user",
"operator"
],
"phoneNumbers": [
{
"type": "mobile",
"number": "111-111-1111"
},
{
"type": "home",
"number": "222-222-2222"
}
]
}
Read ¶
To read a JSON with the Object Model API, you create a JsonReader
instance with the static method Json.createReader
. This method expects either an InputStream
or a Reader
as an argument. Based on the form of the JSON you read the JSON with readObject()
or readArray()
into memory.
These methods return a JsonObject
resp. JsonArray
instance which gives you access to the data. These classes provide get...()
methods to
retrieve the values of keys and array entries.
JsonObject
implements the Map interface, so you can use all methods you know from Map, for example, jsonObject.keySet()
to list all keys.
JsonArray
is a subinterface of Collection
, Iterable
, and List
.
Note that the get...()
methods return a NullPointerException
if you are trying to access a non-existing key.
To prevent that, you check if the key exists with containsKey()
.
String input = "{\"id\":1234,\"active\":true,\"name\":\"Duke\",\"password\":null,\"roles\":[\"admin\",\"user\",\"operator\"],\"phoneNumbers\":[{\"type\":\"mobile\",\"number\":\"111-111-1111\"},{\"type\":\"home\",\"number\":\"222-222-2222\"}]}";
try (JsonReader reader = Json.createReader(new StringReader(input))) {
JsonObject root = reader.readObject();
int id = root.getInt("id");
boolean active = root.getBoolean("active");
String name = root.getString("name");
System.out.println("id: " + id);
System.out.println("active: " + active);
System.out.println("name: " + name);
JsonValue password = root.get("password");
if (password == JsonValue.NULL) {
System.out.println("password is null");
}
JsonArray roles = root.getJsonArray("roles");
for (int i = 0; i < roles.size(); i++) {
String role = roles.getString(i);
System.out.println(" " + role);
}
if (!root.containsKey("firstName")) {
System.out.println("does not contain firstName");
}
// String firstName = root.getString("firstName");
// throws NullPointerException
JsonArray phoneNumbers = root.getJsonArray("phoneNumbers");
for (JsonValue phoneNumber : phoneNumbers) {
if (phoneNumber.getValueType() == ValueType.OBJECT) {
JsonObject obj = phoneNumber.asJsonObject();
System.out.println(obj.getString("type"));
System.out.println(obj.getString("number"));
}
}
}
Streaming API ¶
The streaming API consists of the interfaces JsonParser
and JsonGenerator
. The parser is used for reading JSON in a streaming fashion, and the generator is used for creating JSON.
Write ¶
To write a JSON with the Streaming API, you need an instance of JsonGenerator
, which the static method Json.createGenerator
creates. The method expects either an OutputStream
or a Writer
as an argument. As the name of the API implies, you create the JSON in a streaming fashion, and the generator writes continuously into the given output stream or writer. For example writeStartObject()
writes the character {
and writeStartArray()
writes [
as soon as they are called.
The generator provides overloaded write()
methods for each JSON datatype. The writeNull()
method can be used if you want to create a key
with a null value.
Make sure that you close every start object (writeStartObject
) and array (writeStartArray
) with writeEnd()
.
The generator throws an exception if the number of writeStart* and writeEnd calls do not match.
package ch.rasc.jsonp;
import java.util.Map;
import jakarta.json.Json;
import jakarta.json.stream.JsonGenerator;
public class StreamWrite {
public static void main(String[] args) {
var properties = Map.of(JsonGenerator.PRETTY_PRINTING, Boolean.FALSE);
try (JsonGenerator jg = Json.createGeneratorFactory(properties)
.createGenerator(System.out)) {
jg.writeStartObject().write("id", 1234).write("active", true).write("name", "Duke")
.writeNull("password").writeStartArray("roles").write("admin").write("user")
.write("operator").writeEnd().writeStartArray("phoneNumbers").writeStartObject()
.write("type", "mobile").write("number", "111-111-1111").writeEnd()
.writeStartObject().write("type", "home").write("number", "222-222-2222")
.writeEnd().writeEnd().writeEnd();
}
}
}
Output:
{
"id": 1234,
"active": true,
"name": "Duke",
"password": null,
"roles": [
"admin",
"user",
"operator"
],
"phoneNumbers": [
{
"type": "mobile",
"number": "111-111-1111"
},
{
"type": "home",
"number": "222-222-2222"
}
]
}
Read ¶
To read a JSON with the Streaming API, you need to create an instance of JsonParser
with the static method Json.createParser
. This class expects either an InputStream or a Reader as an argument.
With the JsonParser
in hand, an application can move forward through the JSON from one token to the next. In contrast to the Object Model API, the streaming API does not
read the whole JSON into memory instead of an application requests the next token with next()
or skips a section of the JSON with skip*()
and the parser loads the data when required from the Reader/InputStream.
For each token, the parser returns an Event
object that describes the current token. next()
returns the current event.
To read the value, you then call a get...()
method from the JsonParser
object.
JsonParser
also provides the skipArray()
and skipObject()
methods which instruct the parser to skip a whole array/object and advance the parser to the end of the array resp. object.
String input = "{\"id\":1234,\"active\":true,\"name\":\"Duke\",\"password\":null,\"roles\":[\"admin\",\"user\",\"operator\"],\"phoneNumbers\":[{\"type\":\"mobile\",\"number\":\"111-111-1111\"},{\"type\":\"home\",\"number\":\"222-222-2222\"}]}";
try (JsonParser parser = Json.createParser(new StringReader(input))) {
while (parser.hasNext()) {
Event event = parser.next();
switch (event) {
case START_OBJECT:
System.out.println("START_OBJECT");
break;
case END_OBJECT:
System.out.println("END_OBJECT");
break;
case START_ARRAY:
System.out.println("START_ARRAY");
break;
case END_ARRAY:
System.out.println("END_ARRAY");
break;
case KEY_NAME:
System.out.print("KEY_NAME: ");
System.out.println(parser.getString());
break;
case VALUE_NUMBER:
System.out.print("VALUE_NUMBER: ");
System.out.println(parser.getLong());
break;
case VALUE_STRING:
System.out.print("VALUE_STRING: ");
System.out.println(parser.getString());
break;
case VALUE_FALSE:
System.out.println("VALUE_FALSE");
break;
case VALUE_TRUE:
System.out.println("VALUE_TRUE");
break;
case VALUE_NULL:
System.out.println("VALUE_NULL");
break;
default:
System.out.println("something went wrong: " + event);
break;
}
}
}
Here another example that demonstrates how you can use the Streaming API to read data from a JSON. The example loads a GeoJSON file from usgs.gov with the earthquakes from the last 30 days. The program extracts the magnitude and the place of each earthquake and prints them into the console.
String url = "https://earthquake.usgs.gov/earthquakes/feed/v1.0/summary/1.0_month.geojson";
var client = HttpClient.newHttpClient();
var request = HttpRequest.newBuilder().GET().uri(URI.create(url)).build();
HttpResponse<InputStream> response = client.send(request,
BodyHandlers.ofInputStream());
try (JsonParser parser = Json.createParser(response.body())) {
while (parser.hasNext()) {
Event event = parser.next();
if (event == Event.KEY_NAME) {
String key = parser.getString();
if ("mag".equals(key)) {
parser.next();
System.out.println(parser.getBigDecimal());
}
else if ("place".equals(key)) {
parser.next();
System.out.println(parser.getString());
System.out.println();
}
}
}
}
JSON Pointer ¶
JSON Pointer is an implementation of the JavaScript Object Notation (JSON) Pointer standard.
A JSON Pointer is a string that references an element within a JSON document. With a JSON pointer, an application can retrieve a value, and it can modify a JSON document.
To create a pointer, you call the static method Json.createPointer
and pass the pointer expression as an argument. The factory method returns an instance of JsonPointer
.
Read ¶
The following example reads the earthquake GeoJSON file from usgs.gov. The file looks like this:
{
type: "FeatureCollection",
metadata: {
...
},
bbox: [
...
],
features: [
{
type: "Feature",
properties: {
mag: Decimal,
place: String,
time: Long Integer,
updated: Long Integer,
...
The application uses three JSON Pointers to access the complete features
array and to read the magnitude
and place
of the first element of the array. Note that every pointer expression starts with /
which
denotes the root object. The special /<number>/
syntax can be used to access the nth element of an array.
String url = "https://earthquake.usgs.gov/earthquakes/feed/v1.0/summary/1.0_month.geojson";
var client = HttpClient.newHttpClient();
var request = HttpRequest.newBuilder().GET().uri(URI.create(url)).build();
HttpResponse<InputStream> response = client.send(request,
BodyHandlers.ofInputStream());
JsonPointer allPointer = Json.createPointer("/features");
JsonPointer magPointer = Json.createPointer("/features/0/properties/mag");
JsonPointer placePointer = Json.createPointer("/features/0/properties/place");
try (JsonReader reader = Json.createReader(response.body())) {
JsonObject root = reader.readObject();
JsonArray all = (JsonArray) allPointer.getValue(root);
System.out.println("number of earthquakes: " + all.size());
System.out.println(magPointer.getValue(root));
System.out.println(placePointer.getValue(root));
}
Modify ¶
A JSON Pointer can not only be used for retrieving values, but an application can also use JSON Pointers to modify JSON documents. Note that the JSON Pointer API only works with the Object Model API because it needs access to the whole JSON document.
Add ¶
To insert a new key/value pair, you create a pointer to a non-existing key and call the method add()
. The method
expects a JsonObject
instance and a value object.
The following example inserts the new key email
into the JSON document.
JsonObject jsonObject = Json.createObjectBuilder().add("id", 1234).add("active", true)
.addNull("password").add("roles", Json.createArrayBuilder().add("admin")).build();
System.out.println(jsonObject);
// {"id":1234,"active":true,"password":null,"roles":["admin"]}
// add
JsonPointer emailPointer = Json.createPointer("/email");
JsonObject modifiedJsonObject = emailPointer.add(jsonObject,
Json.createValue("test@test.com"));
System.out.println(modifiedJsonObject);
// {"id":1234,"active":true,"password":null,"roles":["admin"],"email":"test@test.com"}
Add value to an Array ¶
For adding elements to an array, the API supports the special -
syntax.
The minus sign signifies that an element should be pushed to the end of an array.
The following code adds dev
to the existing array. Without the -
syntax, you need to repeat all existing array elements. If you don't do that
the add()
method overwrites the existing array values.
With the special /roles/-
pointer, you only need to specify the new value and add()
inserts the new element at the end of the array.
JsonPointer rolesPointer = Json.createPointer("/roles");
modifiedJsonObject = rolesPointer.add(jsonObject,
Json.createArrayBuilder().add("admin").add("dev").build());
System.out.println(modifiedJsonObject);
// {"id":1234,"active":true,"password":null,"roles":["admin","dev"]}
JsonPointer addRolesPointer = Json.createPointer("/roles/-");
modifiedJsonObject = addRolesPointer.add(jsonObject, Json.createValue("dev"));
System.out.println(modifiedJsonObject);
// {"id":1234,"active":true,"password":null,"roles":["admin","dev"]}
Remove ¶
To remove a key, create a pointer to an existing key and call remove()
.
JsonPointer passwordPointer = Json.createPointer("/password");
modifiedJsonObject = passwordPointer.remove(jsonObject);
System.out.println(modifiedJsonObject);
// {"id":1234,"active":true,"roles":["admin"]}
Replace ¶
To replace a value call replace()
and pass the new value as second argument.
JsonPointer activePointer = Json.createPointer("/active");
modifiedJsonObject = activePointer.replace(jsonObject, JsonValue.FALSE);
System.out.println(modifiedJsonObject);
// {"id":1234,"active":false,"password":null,"roles":["admin"]}
Test ¶
The JSON Pointer API provides the containsValue()
method to check if
the pointer expression points to a key that exists in the JSON document.
System.out.println(activePointer.containsValue(jsonObject));
JsonPointer lastNamePointer = Json.createPointer("/lastName");
if (!lastNamePointer.containsValue(jsonObject)) {
System.out.println("lastName does not exist");
}
JSON Merge Patch ¶
JSON Merge Patch is an implementation of the JSON Merge Patch standard.
A merge patch is a way to describe the differences between two JSON documents. JSON Merge patch is itself a JSON document. It can be used to keep two JSON documents in sync between two or more parties. Useful for exchanging huge JSON documents. Instead of sending the whole document each time when something changes, an application can create a merge patch and send that to the other parties. The receivers then apply the merge patch to their version of the JSON document and get as a result an up-to-date JSON document that looks the same as the source.
JSON Merge Patch only works with the Object Model API. To create a merge patch, you call the
method Json.createMergeDiff()
and pass the source and target object as the arguments.
The following example removes the active
key from the document, and adds a new
value to the roles array.
JsonObject source = Json.createObjectBuilder().add("id", 1234).add("active", true)
.add("roles", Json.createArrayBuilder().add("admin")).build();
System.out.println(source);
// {"id":1234,"active":true,"roles":["admin"]}
JsonObject target = Json.createObjectBuilder().add("id", 1234)
.add("roles", Json.createArrayBuilder().add("admin").add("dev")).build();
System.out.println(target);
// {"id":1234,"roles":["admin","dev"]}
JsonMergePatch mergePatch = Json.createMergeDiff(source, target);
System.out.println(mergePatch.toJsonValue());
// {"active":null,"roles":["admin","dev"]}
A merge patch can also be created from a JSON string with Json.createMergePatch()
.
try (JsonReader reader = Json.createReader(
new StringReader("{\"active\":null,\"roles\":[\"admin\",\"dev\"]}"))) {
JsonMergePatch createdMergePatch = Json.createMergePatch(reader.readValue());
JsonValue modifiedSource = createdMergePatch.apply(source);
System.out.println(modifiedSource);
// {"id":1234,"roles":["admin","dev"]}
}
To apply a merge patch you call the apply()
method of the JsonMergePatch
instance.
The method expects a JSON document and returns the changed document.
Shortcomings ¶
In the example above we have seen the merge patch document {"active":null,"roles":["admin","dev"]}
that describes the differences between the two JSON documents:
- Source:
{"id":1234,"active":true,"roles":["admin"]}
- Target:
{"id":1234,"roles":["admin","dev"]}
This reveals some shortcomings of the JSON Merge Patch format.
The special null
value is used for removing key/value pairs, so there is no way to set a key to the null
value.
Another shortcoming is that a merge patch cannot describe array manipulations. If an
application adds, removes, or changes elements of an array, the merge patch has to include the entire array. In the
example above, we only added dev
to the roles array, but the merge patch has to include the contents of the
entire array in the merge patch. This gets worse if you add or remove one element in an array with hundreds or thousands of elements.
Another problem is that applying a merge patch never results in an error. Any malformed patch will be merged.
JSON Patch ¶
JSON Patch is an implementation of the JavaScript Object Notation (JSON) Patch standard.
Like JSON Merge Patch, JSON Patch is a standard to describe changes between two JSON documents. JSON Patch does this by listing an array of operations that applied to a target JSON document result in the source document.
A JSON Patch itself is a JSON document that looks like this.
[
{
"op":"replace",
"path":"/id",
"value":4321
},
{
"op":"remove",
"path":"/active"
},
{
"op":"add",
"path":"/roles/1",
"value":"dev"
}
]
A JSON Patch JSON is always an array of objects that contain the op
and path
keys and an optional value
key.
op
describes the operation that should be performed. JSON Patch supports the operations add, copy, move, remove, replace and test.
The path
key describes which part of the JSON document is affected by the operation and is a JSON Pointer expression.
The optional value
key is a standard JSON value (string, number, object, array, boolean, null) that will be used in the operation.
Only required for the add and replace operation.
To create a JSON Patch, an application calls the static Json.createDiff()
method and passes two documents as arguments.
JsonObject source = Json.createObjectBuilder().add("id", 1234).add("active", true)
.add("roles", Json.createArrayBuilder().add("admin")).build();
System.out.println(source);
// {"id":1234,"active":true,"roles":["admin"]}
JsonObject target = Json.createObjectBuilder().add("id", 4321)
.add("roles", Json.createArrayBuilder().add("admin").add("dev")).build();
System.out.println(target);
// {"id":4321,"roles":["admin","dev"]}
JsonPatch patch = Json.createDiff(source, target);
System.out.println(patch.toString());
// [{"op":"replace","path":"/id","value":4321},{"op":"remove","path":"/active"},{"op":"add","path":"/roles/1","value":"dev"}]
A JSON Patch object can also be created from a JSON string with the Json.createPatch
method.
try (JsonReader reader = Json.createReader(new StringReader(
"[{\"op\":\"replace\",\"path\":\"/id\",\"value\":4321},{\"op\":\"remove\",\"path\":\"/active\"},{\"op\":\"add\",\"path\":\"/roles/1\",\"value\":\"dev\"}]"))) {
JsonPatch createdPatch = Json.createPatch(reader.readArray());
JsonValue modifiedSource = createdPatch.apply(source);
System.out.println(modifiedSource);
// {"id":4321,"roles":["admin","dev"]}
}
To apply the changes, an application has to call the apply()
method of the JsonPatch
instance.
The method expects a JSON document as an argument and returns the changed document.
Another way to create a JSON Patch is with the JsonPatchBuilder
.
With the builder, you programmatically create patches.
JsonPatch builtPatch = Json.createPatchBuilder().replace("/id", 4321)
.add("/roles/1", "dev").remove("/active").build();
JsonValue modifiedSource = builtPatch.apply(source);
System.out.println(modifiedSource);
// {"id":4321,"roles":["admin","dev"]}
The special JSON Pointer syntax -
can be used to push a new element to the end of an array.
builtPatch = Json.createPatchBuilder().add("/roles/-", "dev").copy("/uuid", "/id")
.move("/enabled", "/active").build();
modifiedSource = builtPatch.apply(source);
System.out.println(modifiedSource);
// {"id":1234,"roles":["admin","dev"],"uuid":1234,"enabled":true}
The test()
operation can be used to check for a value before
the patch gets applied. The following code only applies the changes when id contains the value 4321.
apply()
throws a JsonException
exception if the test fails.
try {
builtPatch = Json.createPatchBuilder().test("/id", 4321).add("/roles/-", "dev")
.copy("/uuid", "/id").move("/enabled", "/active").build();
modifiedSource = builtPatch.apply(source);
System.out.println(modifiedSource);
}
catch (JsonException e) {
System.out.println(e.getMessage());
// The JSON Patch operation 'test' failed for path '/id' and value '4321'
}
}
This concludes the journey into JSR 374.
The source code of the presented examples is stored in this GitHub repository:
https://github.com/ralscha/blog2019/tree/master/json-p