Interface JsonModel<N>
- Type Parameters:
N- the node type of the caller's JSON representation
- All Known Implementing Classes:
JacksonJsonModel
Gives jsonlens read access to JSON values in the caller's own representation.
jsonlens has no JSON value types. A caller implements this interface for the node type
N of their JSON library, for example JsonNode of Jackson,
JsonElement of Gson, or Object for Map and List values.
The evaluator then walks the caller's values through this interface. It does not copy or
convert the values, and the result nodes are the caller's own node instances.
Contract
A model is well-formed if it obeys the rules below. For a well-formed model, jsonlens never throws an exception.
jsonlens promises:
- No null arguments. jsonlens never calls a method of a model with a
nullargument. - Kinds. jsonlens calls a method that is specific to a kind only for a node of
that kind. For example, it calls
arrayLength(Object)only whenkind(Object)givesJsonKind.ARRAY.
A well-formed model obeys these rules:
- No exceptions. No method throws an exception.
- No null results. No method returns
null, and each node is a non-null reference. A missing element or member isMaybe.None. JSONnullis a present node of kindJsonKind.NULL. A model for a representation that uses Javanullfor JSONnull, such asMapandList, must use a sentinel object. - Stable values. The value of a node does not change during an evaluation.
- Equal nodes. A model can make a new node object each time it reads a value, so two
reads of the same value can give two objects. The two nodes must be equal by
equals, and have the samehashCode. jsonlens does not compare nodes by reference. - Thread safety. If more than one thread uses the same model, the model is safe for concurrent reads. A model without state obeys this rule.
Methods
A model must implement six methods. The other methods have defaults that use the six, so a
model overrides them only to be faster. An override must give the same result as the default.
The test kit jsonlens-model-testkit checks this for each model.
| Method | For kind | Implement | Use in the evaluator |
|---|---|---|---|
kind(Object) |
all | required | the kind of each node |
arrayLength(Object) |
JsonKind.ARRAY |
required | index and slice selectors, length() |
element(Object, int) |
JsonKind.ARRAY |
required | index, slice, and wildcard selectors |
memberCursor(Object) |
JsonKind.OBJECT |
required | wildcard, filter, and descendant segments, equality |
stringValue(Object) |
JsonKind.STRING |
required | comparisons, length(), match(), search() |
numberValue(Object) |
JsonKind.NUMBER |
required | comparisons |
memberCount(Object) |
JsonKind.OBJECT |
default: a walk of the cursor | length(), object equality |
member(Object, JsonString) |
JsonKind.OBJECT |
default: a walk of the cursor | name selectors, singular queries |
members(Object) |
JsonKind.OBJECT |
default: a stream over the cursor | — |
compareNumbers(JsonNumber, JsonNumber) |
numbers | default: the exact values | comparisons, equality |
equal(Object, Object) |
all | default: no recursion | equality of objects and arrays |
hasDuplicate(Object, JsonString) |
JsonKind.OBJECT |
default: a walk of the cursor | JSON Pointer and JSON Patch |
Comparison of numbers
The model owns the order of numbers, as a Comparator does. One method sees both numbers, so
a model can compare two numbers of its own representation without conversion. The default
method compares the exact values, so a model does not have to
override it. An override must obey one invariant: for all numbers a and b,
compareNumbers(a, b) has the same sign as a.exactValue().compareTo(b.exactValue()).
// A number type of the caller's representation.
record LongValue(long value) implements JsonNumber {
@Override
public JsonDecimal exactValue() {
return JsonDecimal.of(value);
}
}
static final class LongValueModel extends CollectionsModel {
@Override
public JsonNumber numberValue(Object number) {
return new LongValue(((Number) number).longValue());
}
@Override
public int compareNumbers(JsonNumber a, JsonNumber b) {
if (a instanceof LongValue x && b instanceof LongValue y) {
return Long.compare(x.value(), y.value()); // without a conversion
}
return super.compareNumbers(a, b); // other numbers, for example number literals
}
}
Example
This model reads values made of Map, List, String, Number, and Boolean. Java null
becomes a sentinel object, because a node must not be null:
// A model for values made of Map, List, String, Number, and Boolean.
// JSON null is the sentinel JsonNull.NULL, because a node must not be Java null.
enum JsonNull { NULL }
static class CollectionsModel implements JsonModel<Object> {
@Override
public JsonKind kind(Object node) {
return switch (node) {
case Map<?, ?> map -> JsonKind.OBJECT;
case List<?> list -> JsonKind.ARRAY;
case String string -> JsonKind.STRING;
case Number number -> JsonKind.NUMBER;
case Boolean bool -> bool ? JsonKind.TRUE : JsonKind.FALSE;
default -> JsonKind.NULL; // JsonNull.NULL
};
}
@Override
public int arrayLength(Object array) {
return ((List<?>) array).size();
}
@Override
public Maybe<Object> element(Object array, int index) {
List<?> list = (List<?>) array;
return index >= 0 && index < list.size() ? Maybe.some(node(list.get(index))) : Maybe.none();
}
@Override
public MemberCursor<Object> memberCursor(Object object) {
Iterator<? extends Map.Entry<?, ?>> entries = ((Map<?, ?>) object).entrySet().iterator();
return new MemberCursor<>() {
private Map.Entry<?, ?> current = Map.entry("", JsonNull.NULL);
@Override
public boolean next() {
if (!entries.hasNext()) {
return false;
}
current = entries.next();
return true;
}
@Override
public JsonString name() {
return JsonString.of((String) current.getKey());
}
@Override
public Object value() {
return node(current.getValue());
}
};
}
@Override
public JsonString stringValue(Object string) {
// JsonString.of does not copy the String.
return JsonString.of((String) string);
}
@Override
public JsonNumber numberValue(Object number) {
// The model chooses the representation. JsonNumber.of covers the common types.
return switch (number) {
case BigDecimal decimal -> JsonNumber.of(decimal);
case BigInteger integer -> JsonNumber.of(integer);
case Double d -> JsonNumber.of(d.doubleValue()).orElse(JsonNumber.of(0)); // NaN is not JSON
default -> JsonNumber.of(((Number) number).longValue());
};
}
// Optional: these three methods have defaults that walk the cursor. A Map does it faster.
@Override
public int memberCount(Object object) {
return ((Map<?, ?>) object).size();
}
@Override
public Maybe<Object> member(Object object, JsonString name) {
Map<?, ?> map = (Map<?, ?>) object;
String key = JsonString.copyOf(name);
return map.containsKey(key) ? Maybe.some(node(map.get(key))) : Maybe.none();
}
@Override
public boolean hasDuplicate(Object object, JsonString name) {
return false; // A Map cannot hold duplicate names.
}
// Replaces Java null with the sentinel.
private static Object node(Object value) {
return value == null ? JsonNull.NULL : value;
}
}
Give the model to the evaluator with the root value:
Map<String, Object> root = new LinkedHashMap<>();
root.put("name", "jsonlens");
root.put("tags", Arrays.asList("json", null, "rfc9535"));
JsonPathQuery query = JsonPathParser.standard().parse("$.tags[?@ == null]").orElse(null);
List<Node<Object>> nodes = JsonPathEvaluator.standard()
.evaluate(query, root, new CollectionsModel())
.orElse(List.of());
// nodes: [Node[value=NULL, path=$['tags'][1]]]
The module ca.marcusdunn.jsonlens.jackson has a model for Jackson 3, and the Kotlin module
jsonlens-kotlinx-serialization has a model for kotlinx.serialization.
-
Method Summary
Modifier and TypeMethodDescriptionintarrayLength(N array) Returns the number of elements in an array.default intCompares two numbers by their mathematical values (RFC 9535, Section 2.3.5.2.2).Returns the element at a zero-based index.static <A,B> boolean Tells if two values of two models are equal, as JSON values.default booleanTells if two values are equal, as JSON values.default booleanhasDuplicate(N object, JsonString name) Tells if an object has more than one member with a name.Returns the kind of a node.member(N object, JsonString name) Returns the value of the member with a given name.default intmemberCount(N object) Returns the number of members in an object.memberCursor(N object) Returns a cursor over the members of an object.Returns the members of an object as name/value properties, in the order of the cursor.numberValue(N number) Returns the value of a number.stringValue(N string) Returns the value of a string.
-
Method Details
-
kind
-
arrayLength
Returns the number of elements in an array.- Parameters:
array- a node of kindJsonKind.ARRAY- Returns:
- the number of elements, zero or more
-
element
Returns the element at a zero-based index.- Parameters:
array- a node of kindJsonKind.ARRAYindex- a zero-based index. It can be outside the array.- Returns:
- the element, or
Maybe.Noneif the index is outside the array
-
memberCursor
Returns a cursor over the members of an object.
The cursor is the one way to read the members: the default methods for objects use it. The order of the cursor is the order in which the evaluator selects the children of the object. RFC 9535 does not specify this order. The order must be the same each time the method is called for the same object. The cursor gives each member, also a member with the name of an earlier member.
The names can read the representation directly, as
stringValue(Object)does. The evaluator keeps a name in the Normalized Path of each result node, so a name must stay valid while the values of the model are valid.- Parameters:
object- a node of kindJsonKind.OBJECT- Returns:
- a new cursor before the first member
-
memberCount
Returns the number of members in an object.
The default method counts the members of the cursor. A model overrides it if it knows the number without a walk. The result must be the number of members of
memberCursor(Object).- Parameters:
object- a node of kindJsonKind.OBJECT- Returns:
- the number of members, zero or more
-
member
Returns the value of the member with a given name.
Names are equal only if they have the same Unicode scalar values. The model must not apply Unicode normalization or case folding (RFC 9535, Section 2.3.1.2).
The evaluator gives a name from the query, or a name from the cursor of this model or another model.
The default method walks the cursor, and gives the value of the first member with the name. A model overrides it to find the member faster, for example in a hash map. The result must be equal to the result of the default method.
- Parameters:
object- a node of kindJsonKind.OBJECTname- a member name- Returns:
- the member value, or
Maybe.Noneif the object has no member with the name
-
members
Returns the members of an object as name/value properties, in the order of the cursor.
The default method makes a stream over
memberCursor(Object). A model does not have to override it.- Parameters:
object- a node of kindJsonKind.OBJECT- Returns:
- a new stream of the members
-
stringValue
Returns the value of a string.
The result can read the representation directly. For a
String, useJsonString.of(String).- Parameters:
string- a node of kindJsonKind.STRING- Returns:
- the value of the string
-
numberValue
Returns the value of a number.
The model chooses the representation. For the common types, use the factories of
JsonNumber, for exampleJsonNumber.of(long).- Parameters:
number- a node of kindJsonKind.NUMBER- Returns:
- the value of the number
-
compareNumbers
Compares two numbers by their mathematical values (RFC 9535, Section 2.3.5.2.2).
The numbers come from
numberValue(Object)of this model, or from the factories ofJsonNumber, for example for a number literal of the query. The default method compares the exact values, with fast paths that need no conversion: for integers in the range oflong(fromJsonNumber.of(long)or the other factories), for numbers fromJsonNumber.of(double), and for an integer up to 2^53 and adouble.An override compares the numbers of its own representation, and calls the default method (
JsonModel.super.compareNumbers(a, b)) for all other numbers. The result must have the same sign asa.exactValue().compareTo(b.exactValue()).- Parameters:
a- a numberb- another number- Returns:
- a negative value, zero, or a positive value if
ais less than, equal to, or greater thanb
-
equal
Tells if two values are equal, as JSON values.
The rules are the same in RFC 9535 (Section 2.3.5.2.2, the comparison
==) and RFC 6902 (Section 4.6, the operationtest). The values must have the same kind, and:- two strings have the same Unicode scalar values (
JsonString.equal(JsonString, JsonString)); - two numbers have the same mathematical value (
compareNumbers(JsonNumber, JsonNumber)); - two arrays have the same number of elements, and the elements at each index are equal;
- two objects have the same number of members, and for each member of the first object, the second object has a member with the same name and an equal value. The order of the members has no effect.
The default method uses no recursion, so a deep value cannot overflow the stack. A model can override it, for example to compare its own nodes faster. The result must be the same as the result of the default method.
- Parameters:
a- a valueb- another value- Returns:
trueif the values are equal
- two strings have the same Unicode scalar values (
-
equal
Tells if two values of two models are equal, as JSON values.
The rules are the rules of
equal(Object, Object). The models can be different, for example the model of a Jackson tree and the model of a JSON Patch document in a memory-mapped file. The method reads both values directly and makes no copy. It compares numbers withcompareNumbers(JsonNumber, JsonNumber)of the first model, and member names by their Unicode scalar values.- Type Parameters:
A- the node type of the first modelB- the node type of the second model- Parameters:
firstModel- the model of the first valuefirst- the first valuesecondModel- the model of the second valuesecond- the second value- Returns:
trueif the values are equal
-
hasDuplicate
Tells if an object has more than one member with a name.
JSON permits duplicate member names, but their meaning is not defined (RFC 8259, Section 4). RFC 6901 (JSON Pointer) and RFC 6902 (JSON Patch) must detect them: a pointer to a name that is not unique fails, and an operation must have exactly one
opmember.The default method walks the cursor and counts the members with the name. A model whose objects cannot hold duplicate names, for example a model of
Mapvalues, can override it to returnfalsewithout a walk. The result must be the same as the result of the default method.- Parameters:
object- a node of kindJsonKind.OBJECTname- a member name- Returns:
trueif the object has two or more members with the name
-