A lazily parsed JSON virtual tree library.
Virtually here means it represents the "wanted" or "expected" tree structure. On leaf value access it is checked against the actual tree structure underneath. As a result tree navigation is painless and checks only have to be made for and when values are resolved (extracted) from the actual tree.
Lazily parsed means only the parts of the underlying tree that are used during navigation will be parsed into nodes which remember offsets ("pointers") into the original JSON input. Untouched parts of a JSON tree are skipped over until they are used. This makes for a fast and memory efficient implementation that is convenient to query and extend with user types that can handle inputs in MB range without problem.
Creating a (virtual) tree from Java types is handled by the Json
API.
JsonString s = Json.of("hello"); // note: not quoted
JsonBoolean b = Json.of(true);
JsonInteger i = Json.of(42);
JsonNumber d = Json.of(42.01d);
JsonArray a = Json.array(Json::of, List.of(1,2,3));
JsonObject o = Json.object(Json::of, Map.of("a", 4, "b", 2));
Arrays and objects can also be composed in various ways using a builder API.
JsonArray a = Json.array(arr -> arr.addNumber(1).addString("yes"));
a.toJson(); // [1,"yes"]
JsonObject o = Json.object(obj -> obj.addNumber("x", 1).addNumber("y", 5));
o.toJson(); // {"x":1,"y":5}
JsonObject o2 = Json.object(obj -> obj.addArray("points",
arr -> arr.addNumbers(IntStream.range(0,10))));
o2.toJson(); // {"points":[1,2,3,4,5,6,7,8,9]}
Creating a (virtual) tree from JSON is handled by the JsonMixed
and
JsonValue
API.
JsonMixed
is the union type (top type) of all basic JSON types,
while JsonValue
is the base type (bottom type) of all basic JSON types.
The basic types are JsonBoolean
, JsonNumber
, JsonInteger
, JsonString
,
JsonArray
and JsonObject
.
An instance of these represents a virtual tree node.
Their values are then accessed using the appropriate access method on the node.
Representing JSON null
as type is not necessary since in a virtual tree each
node can be of the type that is wanted, not the type actually present.
Hence, the actual value for any type can be undefined or defined null
.
JsonString s = JsonMixed.of("null");
s.exists(); // true, it is a NULL node in the actual tree
s.isUndefined(); // true
s.isNull(); // true
s.isString(); // false
s.type(); // JsonNodeType.NULL
s.string(); // throws exception
Whereas nodes can actually not exist. For example:
JsonString s = JsonMixed.of("{}").getString("x"); // the member "x" as string
s.exists(); // false
s.isUndefined(); // true
s.type(); // null (Java null)
s.string(); // throws exception
When creating a virtual tree using JsonMixed.of
or JsonValue.of
the
input must be valid JSON. This means a string must be quoted.
JsonString s = JsonMixed.of("\"null\"");
s.isString(); // true
s.string(); // "null" (as Java string)
s.type(); // JsonNodeType.STRING
Equally, to create a virtual tree for a complex JSON input:
String json = """
{
"key": "points",
"value": [1,2,3]
}
""";
JsonObject o = JsonMixed.of(json);
The pre-defined basic types of the virtual tree can easily be extended and mixed
with user defined ones. Like the basic types these are defined using interface
s.
Properties are represented as default
methods to build a typical "object model".
interface JsonAddress extends JsonObject {
default String street() {
return getString("street").string();
}
default Integer no() {
return getNumber("no").integer();
}
default JsonString city() {
return getString("city");
}
}
While being convenient to define the user has full control over conversions and weather or not properties are used on the value level or the node level (city). This also allows for logic in the property methods (e.g. for default or parsing).
JsonObject obj = JsonMixed.of("""
{
"street": "Elm",
"no": 11,
"city": "Oslo"
}""");
JsonAddress a = obj.as(JsonAddress.class); // "viewing" the object as address
a.street(); // = "Elm" (Java String)
a.no(); // = 11 (Java Integer)
a.city().exits(); // as city() is merely the node we can check for existence
a.city().string(); // = "Oslo" (Java String)
Entire domain models can easily be created using this approach.
interface JsonUser extends JsonObject {
default JsonAddress address() {
return get( "address", JsonAddress.class );
}
default JsonList<JsonAddress> alternativeAddresses() {
return getList( "alternativeAddresses", JsonAddress.class );
}
}
Since a virtual tree describes the "wanted" structure a common use case is to check the actual input against this target. For that objects can be validated.
JsonObject obj = JsonMixed.of("""
{
"city": "Oslo",
"no": 42
}""");
obj.validate(JsonAddress.class); // node types match, no exception
The basic types all include validation for the JSON node type. This means a string property cannot have an actual value that is a JSON number and so on. To extend the requirements additional validations can be added using annotations.
interface JsonAddress extends JsonObject {
@Required
@Validation( maxLength = 50 )
default String street() {
return getString( "street" ).string();
}
@Validation( required = YesNo.YES, minimum = 1)
default Integer no() {
return getNumber( "no" ).integer();
}
@Validation( maxLength = 50 )
default JsonString city() {
return getString( "city" );
}
}
With the improved JsonAddress
declaration a JSON input that lacks street
would now result in an error and throw an exception when calling
obj.validate(JsonAdress.class)
.
Validation is either given by annotating with @Validation
or using meta
annotation like @Required
. The provided validation follow the JSON schema
validation specification.
Users can simply create their own meta annotations by annotating the annotation
type with @Validation
, for example:
@Target( ElementType.METHOD )
@Retention( RetentionPolicy.RUNTIME )
@Validation( mininum = 1 )
public @interface Positive {}
To check conformity with a given schema type JsonObject::isA
can be used.
The JsonObject::asA
is same as JsonValue::as
with a validation check before
the "cast".
JsonObject obj = JsonMixed.of("""
{
"city": "Oslo",
"no": 0,
"street": "Elm"
}""");
obj.isA(JsonAddress.class); // false, no < 1
JsonAddress a = obj.asA(JsonAddress.class); // throws exception, no < 1
Both isA
and asA
can also be limited to a given set of validation Rule
types.