/boxed-json

Java Library for easy examination and modification of JSON values

Primary LanguageJavaOtherNOASSERTION

Boxed Json Library

This library is for easy extraction of values from JSON with manipulation of the same JSON structure without having to create a copy. All JsonValue types as publicly constructable and extensible.

This Library is based on the GlassFish Open Source Reference Implementation, it is released under the same dual license as the original GlassFish License, which is duplicated in this repository LICENSE.

Build status Maven Central status

Requirements

  • Java 8 or above
  • The project is on Maven: com.vladsch.boxed-json
  • dependencies:
    • org.glassfish:javax.json
    • org.jetbrains.annotations

Quick Start

For Maven:

<dependency>
    <groupId>com.vladsch.boxed-json</groupId>
    <artifactId>boxed-json</artifactId>
    <version>LATEST</version>
</dependency>

Easy Access and Modifications

Json objects created by this library do not throw exceptions and succeed all operations while keeping track of the first error and propagating it through all subsequent access/modification methods. It is perfectly valid to access a value nested in a JsonObject without checking for existence or type of intervening values, only testing at the end if the value is valid. This eliminates all the nested if blocks.

Any value can be accessed via the eval(String path) method.

BoxedJsObject jsReply = BoxedJson.from("{"method":"Runtime.consoleAPICalled","params":{"type":"warning","args":[{"type":"string","value":"warning"}],"executionContextId":30,"timestamp":1519047166210.763,"stackTrace":{"callFrames":[{"functionName":"","scriptId":"684","url":"","lineNumber":0,"columnNumber":8}]}}}");
BoxedJsNumber jsExecutionContextId = jsReply.evalJsNumber("params.executionContextId");
BoxedJsString jsFirstArgType = jsReply.evalJsString("params.args[0].type");
BoxedJsString jsFirstFunction = jsReply.evalJsString("params.stackTrace.callFrames[0].functionName");
if (jsExecutionContextId.isValid() && jsFirstArgType.isValid() && jsFirstFunction.isValid()) {
    // change functionName if it is blank to unknown
    if (jsFirstFunction.getString().isEmpty()) {
        jsReply.evalSet("params.stackTrace.callFrames[0].functionName", "unknown");
    }
}

In the above code it is not necessary to test jsReply for validity because its invalid status is propagated to all values derived from it.

JSON in the above code is prettified here:

{
  "method": "Runtime.consoleAPICalled",
  "params": {
    "type": "warning",
    "args": [
      {
        "type": "string",
        "value": "warning"
      }
    ],
    "executionContextId": 30,
    "timestamp": 1519047166210.763,
    "stackTrace": {
      "callFrames": [
        {
          "functionName": "",
          "scriptId": "684",
          "url": "",
          "lineNumber": 0,
          "columnNumber": 8
        }
      ]
    }
  }
}

How To Use

There are two main sets of classes Mutable and Boxed, the former implements mutable JSON arrays and objects, while the latter wraps any JsonValue and provides easy, no exception access with a lot of helper methods.

Converting a JsonValue.NULL to anything but a literal, will result in a BoxedJsValue which will test true for .hadNull() for any subsequent operation results. Similarly, performing an invalid conversion like accessing a literal or an object as an array, will produce a value which will test true for hadInvalid(). Accessing a non-existent element will do the same for hadMissing(). All of these can be converted to .asLiteral(), .asString(), .asNumber(), .asArray() and .asObject().

That said, all BoxedJs... classes will return valid JsonValues for all conversions via .asJs...() with the caveat that if the conversion is not valid then results of all operations will always be some form of an error JsonValue boxed class.

BoxedJson and its various classes are used for easy hacking. boxed JSON classes provide an eval("path") and evalSet('path', value) functions are implemented for fast access to nested elements. The path consists of concatenated parts of object field and array index syntax:

  • .field - object field
  • [10] - array index, for evalSet, an empty [] index means add to the end of array (like Php).

All of the boxed classes can be converted to other JsonValue types, and if this results in an invalid operation, the error will carry over for all further operations. So it is possible to extract a value, convert to array, get a value at an index, convert to object and extract a field, convert to JsonNumber. Only at the end checking if all the operations succeeded by .isValid(). If any of the intervening operations were invalid then the result will be invalid. Unless you are expecting more invalid JSON input, this method results in faster code than having to check for validity at every step only to have it succeed.

For example, to get the frameId value from {"method":"Page.frameStartedLoading","params":{"frameId":"0.1"}} and if it exists perform some operation:

import com.vladsch.boxed.json.*;

class Test {
    static void main(String[] args) {
        BoxedJsValue json = BoxedJson.objectFrom("{\"method\":\"Page.frameStartedLoading\",\"params\":{\"frameId\":\"0.1\"}}");

        BoxedJsString frameId = json.eval("params.frameId").asJsString();
        if (frameId.isValid() && frameId.getString().equals("0.1")) {
            // add an array of positions to params and change frameId
            MutableJsObject jsObject = new MutableJsObject();
            jsObject.put("x", 100);
            jsObject.put("y", 5);
            json.evalSet("params.positions[]", jsObject);
            json.evalSet("params.frameId", "1.0");
        }
        
        String jsonText = json.toString(); // {"method":"Page.frameStartedLoading","params":{"frameId":"1.0","positions":[{"x":100,"y":5}]}}
    }
}

evalSet will walk the json with the given path and set the final target to the value provided. If the requested intermediate value is missing, it will be created. Arrays will only be created if the index part is empty [] or refers to 1 past the last element, 0-based index.

Any errors encountered will result in no modifications being performed. Check the returned value for validity with .isValid(). When chaining evalSet() operations all operations after the first failure will fail because the return value is will be an invalid JSON value.

BoxedJson class provides static methods for convenient conversions via of() to convert JsonValue instances and common Java types: int, long, BigInteger, double, BigDecimal, String, boolean. Use from() to read the json from String, Reader, InputStream.

The resulting json will be a mutable boxed instance which can be modified. For the of() If the passed in value is already based on the MutableJson classes then this instance will be reused. If you want a new mutable copy you have to explicitly created it via the MutableJson.copyOf(JsonValue) to get a deep copy of the JsonValue.

If you only want a boxed wrapper of original JsonValue GlassFish library implementations use the boxedOf or boxedFrom methods instead.

One caveat to keep in mind is that the mutable classes will convert their contained JSON values to mutable on access. Which means that if the value is already mutable, it will be returned unmodified. If you make changes to the contents of this returned value, then the parent container's copy will reflect these changes. If you don't want the parent container's value to be modified, then before making any modifications to it, you need to make a deep copy via MutableJson.copyOf() or BoxedJson.copyOf().

Why another JSON Java library

I needed to hack on Google Chrome Dev Tools WebSocket protocol to make JavaFX WebView debugging work for evaluating console expressions. This meant I needed to get and modify JSON messages between chrome dev tools and the WebView debugger.

Not only is it a pain to get the parts you want because all invalid operations result in class cast exceptions or other exception hell. The resulting code to handle exceptions and validate operations trying to avoid them, seemed like the purpose of my program. Real goal was a small blip against the background of housekeeping noise.

To make it even less pleasant to deal with JSON, the GlassFish library seems to be designed for software development in a locked-down, high security prison environment. None of the value classes are exposed, classes and constructors are package private. Even static methods to create values are package private. If you think this makes for great re-usable design, you must be an Ada programmer at heart.

Replacing a value meant recreating the whole json with the parts you need replaced.

Writing a JSON library was not on my radar but it was either this or spend a ton of time writing validation if statements and debugging exceptions or worse, resort to using regex to replace values. This was more fun and the result was worth the effort.

What's Missing

Tests!

I moved these classes out of my Markdown Navigator plugin for JetBrains IDEs into a separate module. Unfortunately, the tests I have for it are in Kotlin which is much more compact and convenient. I have not gotten around to porting them to Java. IntelliJ IDEA has a single click Java to Kotlin conversion but no reverse option. So it has to be a manual effort.