Dieser Artikel ist auch auf Deutsch verfügbar

I can still remember how at the beginning of my career in software development XML was everywhere. It was used both for configuration files and for text-based exchange formats. Not least as a result of the fact that SOAP and also the browser used XML via AJAX, the use of XML was unavoidable. Already then, XML often had the reputation of being rather rambling and, above all in combination with schemas, also quite complicated. Over the years it was slowly displaced by JSON. Nowadays, at least in my work, XML is no longer relevant.

If however we want to generate or process JSON in Java, we quickly learn that JDK itself does not offer a programming interface for this purpose. The main reason for this is that the limited capacity of the JDK team is not to be overloaded with additional APIs. We therefore have to look elsewhere for a suitable library. As expected, a quick search reveals not just one result but a whole host of libraries to choose from.

This article focuses on the programming models of four different libraries for Java: org.json, Gson, and Jackson as well as JSON-P and JSON-B from the Jakarta EE world. At the end we will also take a quick look at the aspects of performance and security.

org.json

The library org.json exists already since the end of 2010 and was initially implemented by Douglas Crockford, the creator of JSON. One can therefore consider it the reference implementation for JSON in Java.

On balance it is an easy-to-use programming interface that at its core consists of the two classes JSONObject and JSONArray. These map the elements defined in the JSON specification which are not already covered by classes available in Java. These two classes and their constructors and methods are sufficient to programmatically generate JSON. Listing 1 shows the construction of a JSON object with various values. Because the put method returns itself, a compact declaration results from the concatenation of the method calls.

JSONObject json = new JSONObject()
    .put("number", 1)
    .put("object", new JSONObject()
        .put("string", "Hello"))
    .put("boolean", true)
    .put("array", new JSONArray()
        .put(47.11)
        .put("Hello again"));
Listing 1: Programmatic generation of a JSON object with org.json

The parsing of JSON is equally simple. Here we can provide the constructor of JSONObject or JSONArray with an instance of a JSONTokener. This in turn can be generated with a String, Reader, or InputStream. Listing 2 shows how we can parse the JSON structure from Listing 1 from a string.

JSONObject json = new JSONObject(new JSONTokener("""
    {
        "number": 1,
        "object": {
            "string":"Hello"
        },
        "boolean": true,
        "array": [
            47.11,
            "Hello again"
        ]
    }
"""));
Listing 2: Parsing of a JSON string with org.json

There are two ways to write JSON. The first is used when we already have a JSONObject or JSONArray. Here we use, as can be seen in Listing 3, the write method, which we can provide with a Writer and optionally also an indentation factor. Alternatively, we can directly output JSON by means of JSONWriter without having to generate objects first. In Listing 4 we write the already known structure directly to the default output of our process.

JSONObject json = ...;

StringWriter writer = new StringWriter();
json.write(writer, 4, 0);
System.out.println(writer);
Listing 3: Writing a JSONObject in a string
new JSONWriter(System.out)
    .object()
        .key("number").value(1)
        .key("object").object()
            .key("string").value("Hello")
        .endObject()
        .key("boolean").value(true)
        .key("array").array()
            .value(47.11)
            .value("Hello again")
        .endArray()
    .endObject();
Listing 4: Direct writing of JSON to the default output

In order to work in code with a JSONObject or JSONArray, a range of methods are available to us. For example, we can use has or isNull to check whether a field exists and is not null. Although isNull also returns true for fields that do not exist.

In order to query individual field values, we can choose from an array of getXxx methods that return the value in the required Java data type. If in doing so we query a nonexistent field, a JSONException is thrown. In parallel we can therefore use one of the optXxx methods. These do not throw any exceptions but only standard values. Listing 5 shows a few examples for the use of these methods.

JSONObject json = new JSONObject()
    .put("number", 1)
    .put("array", new JSONArray()
        .put(2))
    .put("string", "5")
    .put("null", JSONObject.NULL);

json.isEmpty();           // -> false
json.has("not-there");    // -> false
json.isNull("not-there"); // -> true
json.has("null");         // -> true
json.isNull("null");      // -> true

json.optInt("string");    // -> 5
json.getInt("string");    // -> 5
json.getString("number"); // throws Exception
json.optString("number"); // -> "1"

json.increment("number");     // -> "number": 2
json.append("array", false);  // -> "array" [2, false]
json.accumulate("string", 2); // -> "string": ["1", 2]
Listing 5: Queries and transformations of a JSONObject

In general the methods can be used as one would expect, but I was however surprised in one or two places. For example, the query via getString delivers an exception if the value is a number. The same field queried with optString however returns the value of the number as a string. Vice versa, with both getInt and optInt with a string field, the string is parsed into a number. And the empty string selected as standard value for optString takes some getting used to.

Recently, JSON-Pointer has also been supported for queries. Similarly to XPath for XML, this offers us the possibility to extract values from an object or array with a single expression. Two examples of this can be seen in Listing 6.

JSONObject json = new JSONObject()
    .put("object", new JSONObject()
        .put("string", "Hello"))
        .put("array", new JSONArray()
            .put(47.11)
            .put("Hello again")
            .put(new JSONObject()
                .put("string", "Bye!")));

System.out.println(json.query("/object/string"));
System.out.println(json.query("/array/2/string"));
Listing 6: Queries with JSON Pointer to a JSONObject

Gson

Gson from Google has existed even longer than org.json, namely since 2008. Similarly to org.json, Gson allows the reading, creation, and writing of “generic” JSON objects. The mapping of the types from the JSON specification takes place with the classes JsonObject, JsonArray, JsonPrimitive, and JsonNull, which are all inherited from JsonElement. In everyday use JsonPrimitive and JsonNull are however generally not of relevance, as here we can use the Java primitives and Gson then only converts in these classes internally.

When creating (see Listing 7), it can immediately be seen that we don’t have the option here of directly concatenating the methods, as the methods add and addProperty have void as return type.

JsonObject object = new JsonObject();
object.addProperty("string", "Hello");

JsonArray array = new JsonArray();
array.add(47.11);
array.add("Hello again");

JsonObject json = new JsonObject();
json.addProperty("number", 1);
json.add("object", object);
json.addProperty("boolean", true);
json.add("array", array);
Listing 7: Generation of a JSON object with Gson

In order to parse any present JSON into a JsonElement, we use the class JsonParser. Gson however also offers us a streaming-based solution by means of JsonReader. In this approach we have to jump from token to token ourselves. This option is especially advantageous for the processing of very large data volumes that we don’t want to completely read in the memory. Both options can be seen in Listing 8.

// JsonParser
JsonElement json = JsonParser.parseString("""
    {
        "number": 1,
        "object": {
            "string":"Hello"
        },
        "boolean": true,
        "array": [
            47.11,
            "Hello again"
        ]
    }
""");

// JsonReader
try (JsonReader reader = new JsonReader(
        new StringReader(json.toString()))) {
    reader.beginObject();
    while(reader.hasNext()) {
        System.out.println(reader.nextName());
        reader.skipValue();
    }
}
Listing 8: Reading of JSON with Gson

For the writing of JSON, we can either use JsonWriter to write JSON directly upon generation or serialize our JsonElement by means of the class Gson.

Gson, in contrast to org.json, does not use JSON Pointer, but uses data binding instead. Using data binding and the class Gson we can bind JSON to existing Java objects and continue to write them as JSON. In Listing 9 it can be seen how we first create an instance of our Test class from JSON and then continue to write in JSON.

class Test {
    private int number;
    private String string;

    @Override
    public String toString() {
        return "Test { number=%d, string=%s }".formatted(number, string);
    }
}

Gson gson = new GsonBuilder().create();

Test test = gson.fromJson("""
    {
        "number": 5,
        "string": "Michael"
    }
""", Test.class);

String json = gson.toJson(test);
Listing 9: Data binding with Gson

For this purpose, Gson builds on the fact that the utilized class has a default constructor and then uses reflection to find all fields of the class and the parent class. However, Gson does not yet offer support for records introduced with JDK 16.

In addition, by means of TypeAdapter Gson allows us to define our own mapping logic for types. For example, by default it is also possible to use java.net.URL, which is mapped to a JSON string. Using annotations, it is also possible to use a name in JSON that is different from the Java field name or to limit the binding to specific fields.

Gson can also support us with the evolution of our data formats. In-built support for versioning is provided for this purpose. We annotate fields or classes with @Since and/or @Until and tag them with a version. Upon generation of the Gson class we can then state which version this instance supports. When reading or writing JSON, Gson then only analyzes the fields that are supported in the specified version.

Jackson

Jackson, like Gson, exists since 2008. As far as I can tell it is currently the most commonly used library for the processing of JSON with Java. This is mainly because it is set as default in Spring Boot.

Although at its core Jackson has a streaming-based programming interface including JSON implementation, this is generally not used. Instead, Jackson stands out for its comprehensive and configurable data binding. Listing 10 shows only a snippet of the possibilities.

@JsonTypeInfo(use = JsonTypeInfo.Id.NAME, property = "type")
@JsonSubTypes({
    @JsonSubTypes.Type(value = Bar.class, name = "my-bar"),
    @JsonSubTypes.Type(value = Baz.class, name = "some-other")})
interface Foo {}

record Bar(int prop) implements Foo {}

record Baz(String property) implements Foo {}

record Test(@JsonProperty("nummer") int number,
        String string,
        List<Foo> foos) {}

ObjectMapper om = new ObjectMapper();

Test test = om.readValue("""
    {
        "nummer": 5,
        "string": "Michael",
        "foos": [
            {
                "type": "my-bar",
                "prop": 42
            },
            {
                "type": "some-other",
                "property": "Hello"
            }
        ]
    }
""", Test.class);

String json = om.writeValueAsString(test);
Listing 10: Data binding with Jackson

As can be seen in the listing, Jackson already supports records and can also deal with inheritance. In order to influence the mapping, a host of annotations are available to us. Alongside @JsonProperty shown in the listing, there is for example also @JsonFormat, used to specify the format for a date. With the @JsonView annotation we furthermore have the option, when writing, of excluding different fields of the object depending on the use case without having to create new classes for the serialization for each combination.

Alongside the configuration of the mapping, Jackson itself can be configured in many ways. For example, it can be specified whether fields with a null value are written or omitted, or we can state that in lists in a Java model only one object or value can be written in JSON. Moreover, we can add our own data types to Jackson.

In addition to the core of Jackson there are also a host of additional modules, which broadly speaking can be separated into two fields. On the one hand there are ready-made modules that provide support for additional data types, such as Eclipse Collections or Joda-Time. On the other hand there are the modules that deal above all with different data formats. As Jackson at its core is generic and independent of concrete formats, we can use it for the data binding of formats such as XML, YAML, or Protobuf.

JSON-P and JSON-B

It goes without saying that the world of Jakarta EE also offers support for JSON. Here there is, as usual, a specification that can then be implemented by different libraries.

Similarly to Jackson, the JSON support is separated into two parts. With JSON-P there is a specification that only concerns itself with the reading and writing of JSON. Building on this, JSON-B offers support for data binding. The work of JSON-P (see Listing 11) is reminiscent of org.json and Gson. Even JSON Pointer is supported (see Listing 12), as in org.json.

// Creation
JsonObject json = Json.createObjectBuilder()
    .add("number", 1)
    .add("object", Json.createObjectBuilder()
        .add("string", "Hello"))
    .add("boolean", true)
    .add("array", Json.createArrayBuilder()
        .add(47.11)
        .add("Hello again"))
    .build();

// Reading
try (JsonReader reader = Json.createReader(
        new StringReader(json.toString()))) {
    json = reader.readObject();
}

// Writing
StringWriter writer = new StringWriter();
try (JsonWriter jsonWriter = Json.createWriter(writer)) {
    jsonWriter.write(json);
    System.out.println(writer);
}
Listing 11: Creation, writing, and reading of JSON with JSON-P
JsonObject json = Json.createObjectBuilder()
    .add("object", Json.createObjectBuilder()
        .add("string", "Hello"))
    .add("array", Json.createArrayBuilder()
        .add(47.11)
        .add("Hello again")
        .add(Json.createObjectBuilder()
            .add("string", "Bye!")))
    .build();

System.out.println(Json.createPointer("/object/string").getValue(json));
System.out.println(Json.createPointer("/array/2/string").getValue(json));
Listing 12: JSON Pointer with JSON-P

A particular feature of JSON-P is that it also provides support for JSON Patch. JSON Patch allows us to define operations in JSON that can then be applied to and change a JSON object. Listing 13 shows how we can create such a patch using Builder in Java and apply it to an object.

JsonObject json = Json.createObjectBuilder()
    .add("number", 1)
    .add("boolean", true)
    .add("array", Json.createArrayBuilder()
        .add(47.11)
        .add("Hello again"))
    .build();

JsonPatch patch = Json.createPatchBuilder()
    .add("/added", Json.createObjectBuilder()
        .add("foo", "bar").build())
    .remove("/boolean")
    .replace("/number", 3)
    .replace("/array/1", "Bye")
    .add("/array/2", Json.createObjectBuilder()
        .add("foo", "bar").build())
    .build();

JsonObject patchedJson = patch.apply(json);
Listing 13: JSON Patch with JSON-P

The data binding with JSON-B is the same as in Jackson. We use our classes and can use annotations to adjust individual aspects such as field names. We of course also have the option here of adding adapters for our own types.

As with Gson, records are currently not supported by JSON-B by default. But in only a few simple steps JSON-BRecords we can ensure that these can in fact be used.

Performance

Alongside the programming model and interface of a library, the performance is also often important for the processing of JSON. We must of course gauge for ourselves whether the achieved performance of a library is sufficient for our specific problem or not.

If we don’t want to measure this ourselves, the Java JSON Benchmark can provide us with an initial impression. Already at first glance it can be seen that big differences can occur depending on the choice of test data and whether we are looking at reading or writing.

With regard to this benchmark, Jackson just has the edge over the other three candidates, without having to add an additional module in the form of Jackson Afterburner to increase the performance. The JSON-B reference implementation Yasson in contrast is significantly better placed with writing than with reading.

Of the two significantly smaller libraries org.json and Gson, Gson has a small advantage. But both lie significantly behind Jackson, at least in this benchmark, which surprised me.

The by some margin fastest library in this benchmark, dsljson, relies in contrast to our four libraries on code generation by means of Java annotation processing. Not having to use reflection makes a big difference on the runtime.

Security

The topic of security is particularly important for the reading of JSON. We mostly use the JSON libraries for processing incoming data, for example in a HTTP API. As we generally do not have complete control over the clients here, there is a high risk of attack.

A possible attack vector is the occurrence of a denial of service. Very large or deeply nested JSON objects mean that the parsing takes so long or needs so much memory that the application can no longer respond.

The other possibility involves attempting to force a subclass during data binding with inheritance for an object. This subclass is then used to execute malicious code. The article by Brian Vermeer provides a better understanding of this type of attack.

For both of these attack vectors we have to get to grips not only with the libraries themselves but also their selected configurations. If for example for reading we never use objects with inheritance, we can turn off the respective feature and thus effectively preclude the second attack vector.

In addition to secure configuration, which all of the libraries considered in this article should have by default, it is also important to regularly update the libraries to the latest version.

Conclusion

In this article we looked at four different libraries for the use of JSON in Java – org.json, Gson, Jackson, and JSON-P/-B. Whether simple reading and writing with org.json, data binding with Gson, generic data format solutions with Jackson, or standardized Jakarta EE API with JSON-B, an appropriate solution is available for every requirement.

In addition to the programming model we also considered the performance by means of a common benchmark. This highlighted that there are other libraries, such as dsljson, that should be considered, depending on the concrete case.

It goes without saying that security also plays an important role. This is especially relevant with regard to the reading of JSON, as the data frequently comes from sources that cannot be controlled or verified by us. With regular updates and appropriate configuration of the library these attack vectors can be significantly reduced.

The code for the listings presented in this article can be found at https://github.com/mvitz/javaspektrum-json, to avoid having to type it all out should you wish to experiment with it.