The Document API

The "Document API" is the "full" intermediate structured data tree. This is a sealed hierarchy of types that looks like this:

DocumentElement

  NonValueElement
    FormattingElement
    CommentElement

  ValueElement
    ObjectElement
    ArrayElement
    PrimitiveElement
      BooleanElement
      DoubleElement
      LongElement
      StringElement
      NullElement

  KeyValuePairElement

Note that all of the above types are public API, and that DoubleElement and LongElement are distinct types, which can be hit with a pattern-matching switch. Additionally, KeyValuePairs inside an ObjectElement are exposed, which can be useful for setting or inspecting prologue comments.

Non-Value Elements

There are two kinds of things which are "not a value" in an object tree, and these things are always attached to (or "attributed to") ValueElements.

Formatting is generally some kind of whitespace in the document. Currently the only usage recommended is FormattingElement.NEWLINE, but you are currently free to emit any additional whitespace you want.

Comments are supported in many types - although a destination format may not support them, in which case the comment may be transformed into a different type:

JSON5 supports all these types, so you can expect the default JsonWriter to emit these comments unharmed.

CommentElement is mutable, so you can setValue("") to change the comment text. Currently you cannot set the comment type in an existing document, you would replace the comment with a new one of the desired type instead.

Key-Value Pairs

These are a special category of DocumentElement that are neither value nor non-value elements. As such, they can't be in places where Value is expected, like "as a list element" or "the root element of a document". KeyValuePairElement implements Map.Entry<String, ValueElement> and consists of the key combined with the ValueElement as you might expect. You'll see these primarily in the Iterator that ObjectElement provides.

Value Elements

Anything that is vald as a root element in a JSON file is a ValueElement. See above for hierarchy.

Values are Optionals

Because JSON null is a JSON value, and all values are nullable in JSON, the Document API treats all values as if they were Optionals.

The following methods unconditionally return nonnull ValueElements. However, those elements may be equivalent to an Optional.empty().

PrimitiveElement ObjectElement::getPrimitive(String key);
ArrayElement     ObjectElement::getArray(String key);
ObjectElement    ObjectElement::getObject(String key);

PrimitiveElement ArrayElement::getPrimitive(int index);
ArrayElement     ArrayElement::getArray(int index);
ObjectElement    ArrayElement::getObject(int index);

This makes it possible to dive several levels deep for a given piece of data, and only have to deal with the optional-unwrap once:

ObjectElement root = Jankson.readJson(in);
PrimitiveElement connectionTimeout = root
    .getArray("servers")
    .getObject(0)
    .getObject("connections")
    .getPrimitive("timeout");
// ConnectionTimeout may be PrimitiveElement.ofNull() at this point,
// but it will never be *Java* null.

If you'd prefer to handle the information about presence or absence of keys yourself, you can use the "try" versions (such as ObjectElement::tryGetPrimitive(String key)) to get an Optional containing your element. If the Optional is empty, there was no mapping for that key or index. If it is present, and the object was PrimitiveElement.ofNull(), then a null was explicitly recorded there.

PrimitiveElements can be projected into Optionals of an expected type

Optional<String>  PrimitiveElement::asString();
Optional<Boolean> PrimitiveElement::asBoolean();
OptionalDouble    PrimitiveElement::asDouble();
OptionalLong      PrimitiveElement::asLong();
OptionalInt       PrimitiveElement::asInt();

// These expect a json string!
Optional<BigInteger> PrimitiveElement::asBigInteger();
Optional<BigDecimal> PrimitiveElement::asBigDecimal();

They can be unwrapped with a default value just like Optional::orElse

String            PrimitiveElement::orElse(String value);
boolean           PrimitiveElement::orElse(boolean value);
double            PrimitiveElement::orElse(double value);
long              PrimitiveElement::orElse(long value);

They can be mapped just like Optional::map

Optional<T>       PrimitiveElement::mapAsString(Function<String, T> mapper);
Optional<T>       PrimitiveElement::mapAsBoolean(Function<Boolean, T> mapper);
Optional<T>       PrimitiveElement::mapAsDouble(DoubleFunction<T> mapper);
Optional<T>       PrimitiveElement::mapAsLong(LongFunction<T> mapper);
Optional<T>       PrimitiveElement::mapAsInt(IntFunction<T> mapper);

They cannot be directly filtered, flat-mapped, or otherwise transformed without first projecting into a typed Optional.

Objects and Arrays are Collections

ObjectElement, even the "empty" object, implements Map<String, ValueElement>. Unlike Map, it can also be directly iterated over, without first calling entrySet(). ArrayElement, even the "empty" array, implements List<ValueElement>. It supports all the new operations, including streaming.