Home | Send Feedback

A look at JSR 374 - Java API for JSON Processing (JSON-P)

Published: 29. August 2019  •  java

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>

pom.xml

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();

ObjectWrite.java

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);
    }

ObjectWrite.java

// {"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);
    }

ObjectWrite.java

{
    "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"));
        }
      }
    }

ObjectRead.java

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();
    }
  }
}

StreamWrite.java

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;
        }
      }
    }

StreamRead.java

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();
          }
        }
      }
    }

StreamReadEarthquakes.java

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));
    }

PointerGet.java


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"}

PointerOperations.java


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"]}

PointerOperations.java


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"]}

PointerOperations.java


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"]}

PointerOperations.java

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");
    }

PointerOperations.java

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"]}

Merge.java

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"]}
    }

Merge.java

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:

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"}]

Patch.java

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"]}
    }

Patch.java

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"]}

Patch.java

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}

Patch.java

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'
    }
  }

Patch.java


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