Package ca.marcusdunn.jsonlens.model


@NullMarked package ca.marcusdunn.jsonlens.model

The interfaces between jsonlens and the caller's JSON representation, and the result types.

jsonlens does not have its own JSON value types. The caller supplies a JsonModel for the node type of their JSON library. The library then reads the caller's nodes directly. It does not copy or convert them.

Type Purpose
JsonModel read access to the JSON values of the caller
JsonFactory builds new values in the representation of the caller
JsonEditor changes the values of the caller in place
JsonKind the seven kinds of JSON value
Property a member of a JSON object: a name and a value
JsonString, JsonNumber, JsonDecimal strings and numbers in the representation of the model, and exact decimals
Result, Maybe a value or an error, and a value that can be absent

This model reads plain Map and List values:

// 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;
    }
}

Errors are values

jsonlens does not use exceptions to report errors. An operation that can fail returns a Result, which is a success value or an error value:

Result<Integer, Problem> port = parsePort("8080");

// Examine a result with a switch. The compiler checks that all cases are present.
String text = switch (port) {
    case Result.Ok(Integer value) -> "port " + value;
    case Result.Err(Problem problem) -> "no port: " + problem.reason();
};

// Or change it with map, flatMap, and fold.
Result<String, Problem> url = port.map(value -> "http://localhost:" + value);
int value = port.orElse(80);

A value that can be absent is a Maybe, not null:

Maybe<String> present = Maybe.some("value");
Maybe<String> absent = Maybe.none();

String text = switch (absent) {
    case Maybe.Some(String value) -> value;
    case Maybe.None() -> "default";
};
int length = present.map(String::length).orElse(0);                        // 5
Result<String, String> required = absent.toResult(() -> "value is missing"); // Err
  • Class
    Description
    An exact decimal number with no limit on its size or on its exponent.
    Changes the caller's JSON values in place.
    Builds nodes of the caller's JSON representation.
    The kind of a JSON value, as RFC 8259 defines it.
    Gives jsonlens read access to JSON values in the caller's own representation.
    A JSON number in a representation that the model chooses.
    A JSON string in a representation that the model chooses.
    A value that can be absent: Maybe.Some value or Maybe.None.
    An absent value.
    A present value.
    Walks the members of one object, in order: the result of JsonModel.memberCursor(Object).
    A member of a JSON object: a name and a value (RFC 9535, Section 1.1).
    Result<V,E>
    The result of an operation that can fail: a success value (Result.Ok) or an error value (Result.Err).
    An error value.
    A success value.