Interface JsonModel<N>

Type Parameters:
N - the node type of the caller's JSON representation
All Known Implementing Classes:
JacksonJsonModel

public interface JsonModel<N>

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 null argument.
  • 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 when kind(Object) gives JsonKind.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 is Maybe.None. JSON null is a present node of kind JsonKind.NULL. A model for a representation that uses Java null for JSON null, such as Map and List, 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 same hashCode. 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 Type
    Method
    Description
    int
    arrayLength(N array)
    Returns the number of elements in an array.
    default int
    Compares two numbers by their mathematical values (RFC 9535, Section 2.3.5.2.2).
    element(N array, int index)
    Returns the element at a zero-based index.
    static <A,B> boolean
    equal(JsonModel<A> firstModel, A first, JsonModel<B> secondModel, B second)
    Tells if two values of two models are equal, as JSON values.
    default boolean
    equal(N a, N b)
    Tells if two values are equal, as JSON values.
    default boolean
    hasDuplicate(N object, JsonString name)
    Tells if an object has more than one member with a name.
    kind(N node)
    Returns the kind of a node.
    default Maybe<N>
    member(N object, JsonString name)
    Returns the value of the member with a given name.
    default int
    memberCount(N object)
    Returns the number of members in an object.
    memberCursor(N object)
    Returns a cursor over the members of an object.
    default Stream<Property<N>>
    members(N 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

      JsonKind kind(N node)
      Returns the kind of a node.
      Parameters:
      node - a node
      Returns:
      the kind of the node
    • arrayLength

      int arrayLength(N array)
      Returns the number of elements in an array.
      Parameters:
      array - a node of kind JsonKind.ARRAY
      Returns:
      the number of elements, zero or more
    • element

      Maybe<N> element(N array, int index)
      Returns the element at a zero-based index.
      Parameters:
      array - a node of kind JsonKind.ARRAY
      index - a zero-based index. It can be outside the array.
      Returns:
      the element, or Maybe.None if the index is outside the array
    • memberCursor

      MemberCursor<N> memberCursor(N object)

      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 kind JsonKind.OBJECT
      Returns:
      a new cursor before the first member
    • memberCount

      default int memberCount(N object)

      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 kind JsonKind.OBJECT
      Returns:
      the number of members, zero or more
    • member

      default Maybe<N> member(N object, JsonString name)

      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 kind JsonKind.OBJECT
      name - a member name
      Returns:
      the member value, or Maybe.None if the object has no member with the name
    • members

      default Stream<Property<N>> members(N object)

      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 kind JsonKind.OBJECT
      Returns:
      a new stream of the members
    • stringValue

      JsonString stringValue(N string)

      Returns the value of a string.

      The result can read the representation directly. For a String, use JsonString.of(String).

      Parameters:
      string - a node of kind JsonKind.STRING
      Returns:
      the value of the string
    • numberValue

      JsonNumber numberValue(N number)

      Returns the value of a number.

      The model chooses the representation. For the common types, use the factories of JsonNumber, for example JsonNumber.of(long).

      Parameters:
      number - a node of kind JsonKind.NUMBER
      Returns:
      the value of the number
    • compareNumbers

      default int compareNumbers(JsonNumber a, JsonNumber b)

      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 of JsonNumber, 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 of long (from JsonNumber.of(long) or the other factories), for numbers from JsonNumber.of(double), and for an integer up to 2^53 and a double.

      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 as a.exactValue().compareTo(b.exactValue()).

      Parameters:
      a - a number
      b - another number
      Returns:
      a negative value, zero, or a positive value if a is less than, equal to, or greater than b
    • equal

      default boolean equal(N a, N b)

      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 operation test). 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 value
      b - another value
      Returns:
      true if the values are equal
    • equal

      static <A,B> boolean equal(JsonModel<A> firstModel, A first, JsonModel<B> secondModel, B second)

      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 with compareNumbers(JsonNumber, JsonNumber) of the first model, and member names by their Unicode scalar values.

      Type Parameters:
      A - the node type of the first model
      B - the node type of the second model
      Parameters:
      firstModel - the model of the first value
      first - the first value
      secondModel - the model of the second value
      second - the second value
      Returns:
      true if the values are equal
    • hasDuplicate

      default boolean hasDuplicate(N object, JsonString name)

      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 op member.

      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 Map values, can override it to return false without a walk. The result must be the same as the result of the default method.

      Parameters:
      object - a node of kind JsonKind.OBJECT
      name - a member name
      Returns:
      true if the object has two or more members with the name