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
-
ClassDescriptionAn exact decimal number with no limit on its size or on its exponent.JsonEditor<N>Changes the caller's JSON values in place.JsonFactory<N>Builds nodes of the caller's JSON representation.The kind of a JSON value, as RFC 8259 defines it.JsonModel<N>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.Maybe<T>A value that can be absent:
Maybe.Somevalue orMaybe.None.Maybe.None<T>An absent value.Maybe.Some<T>A present value.MemberCursor<N>Walks the members of one object, in order: the result ofJsonModel.memberCursor(Object).Property<N>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).Result.Err<V,E> An error value.Result.Ok<V,E> A success value.