Navigation (JSON Path)
SJF4J supports two standardized path syntaxes:
Path-Based Navigation and Mutation
SJF4J provides a unified, JSON-semantic path engine that works on all OBNT nodes.
Use JsonPath
JsonPath represents a compiled, reusable path expression.
JsonPath path = JsonPath.compile("$.user.role");
Object role = path.getNode(node);
// Returns the single matched node (or null if no match)Compile Once, Reuse Many Times
JsonPath path = JsonPath.compile("$.scores[*]");
List<Integer> scores1 = path.find(node, Integer.class);
List<Integer> scores2 = path.find(jo, Integer.class);JsonObject and JsonArray provide convenient shortcut methods for using JsonPath.
These methods follow the naming pattern *ByPath().
For example:
JsonPath.compile("$.user.role").getString(jo);
// Equivalent to:
jo.getStringByPath("$.user.role");Path Evaluation
Path Evaluation Semantics
get*()is strict: exactly one match expected.find*()always returns a list.eval()adapts based on match count.- If the path ends with a function call, the function result is returned.
| JsonPath Method | 0 Match | 1 Match | >1 Match |
|---|---|---|---|
get*() / getAs*() | null | value | ERROR |
find*() / findAs*() | empty list | list | list |
eval() / evalAs*() | null | value | list |
eval() (function at end) | function result | function result | function result |
Strict vs semantic conversion:
get*(node)→ strictgetAs*(node)→ cross-type conversion
Single Result
String json = """
{
"id": 1,
"name": "Alice",
"active": true,
"tags": ["java", "json"],
"scores": [95, 88.8, 0.5],
"user": { "role": "coder"}
}
""";
JsonObject jo = JsonObject.fromJson(json);
String role1 = JsonPath.compile("$.user.role").getString(jo);
String role2 = JsonPath.compile("$.user.role").getAsString(jo);
String role3 = JsonPath.compile("$.user.role").get(jo, String.class);
// They got the same result hereMultiple Results
List<String> tags = jo.findByPath("$.tags[*]", String.class);
List<Integer> firstTwo = jo.findByPath("$.scores[0:2]", Integer.class);Mutation APIs
add(path, value)
- Object member:
- Missing → inserted
- Existing → overwritten
- Array:
- Index in
[0, size]→ inserted - Index > size → ERROR
- Index in
replace(path, value)
- Target must exist
- Otherwise → ERROR
remove(path)
- Cannot remove fields in
POJO - Cannot remove elements in native
ArrayorSet
Use ensurePut(path, value)
Creates intermediate nodes if necessary.
new JsonObject().ensurePutByPath("$.cc.dd[0]", 100);Result:
{
"cc": {
"dd": [100]
}
}If a segment exists but is null, it is treated as non-navigable and replaced with a container.
JSON Path Syntax
SJF4J fully supports the JSON Path (RFC 9535) specification, including filters, functions, descent, unions, slicing, function calls, and so on.
| Syntax | Description | Example |
|---|---|---|
$ | Root | $.name |
@ | Current node (filter only) | @.name |
.name | Object member | $.store.book |
['name'] | Quoted member | $['store'] |
[index] | Array index | $.book[0] |
[*] | Wildcard | $.store[*] |
.. | Recursive descent | $..author |
[start:end] | Array slice | $.*.book[1:3] |
[a,b] | Union | $.book[0,-1] |
[?()] | Filter | $..book[?(@.price < 10)] |
func() | Function call | $..book.size() |
Note: When a function appears at the end of a path, the function result is returned instead of a node list.
Filter Expressions
| Operator | Description | Example |
|---|---|---|
@, $ | Current / Root path | $.a[?(@.b == $.x)] |
==, != | Equality | $..*[?(@.b != 'kilo')].b |
<, <=, >, >= | Numeric comparison | $.a[?@>3.5] |
&&, ||, !, () | Logical operators and grouping | $.o[?@>1 && !(@>4)] |
=~ | Full regular expression match | $[?@.name =~ /^(alice)_\d{2}$/i] |
Example:
List<String> cheapBooks = jo.findByPath("$..book[?(@.price < 10)].title", String.class);
JsonObject subNode = JsonPath.compile("$[?(@.name =~ /^B/)]").getJsonObject(jo);
Integer cnt = jo.evalByPath("$.a[?@>3.5].count()", Integer.class);Built-in Functions
| Function | Description | Example |
|---|---|---|
length() | String/array/object length | $[?length(@.authors) >= 5] |
count() | Node list size | $[?count(@.*.author) >= 5] |
sum() / avg() | Numeric aggregation | $[?sum(@.price) < 20] |
min() / max() | Aggregation | $[?min(@.price) > 3] |
first() / last() | Array selection | $[?first(@.title) =~ /^J/] |
match() | I-Regex (RFC 9485) match | $[?match(@.date, "1974-05-..")] |
search() | I-Regex contains | $[?search(@.author, "[BR]ob")] |
value() | Extract value from NodesType | $[?value(@..color) == "red"] |
In filter context, functions operate on the result of the inner path expression.
Define custom functions
Custom functions can be registered globally via FunctionRegistry:
FunctionRegistry.register(
new FunctionRegistry.FunctionDescriptor("hi", args -> {
return "hi, " + Arrays.toString(args);
})
);
String result = jo.evalByPath("$.hi()", String.class);JSON Pointer
JSON Pointer (RFC 6901) syntax:
- Must start with
/ - Direct navigation only, no
filters,wildcards, orfunctions - Escape rules:
~→~0/→~1
JsonPointer shares the same evaluation and mutation APIs as JsonPath, but only accepts RFC 6901 pointer expressions.
JsonPointer.compile("/scores/2").remove(jo);
String s = jo.getStringByPath("/scores/3");Stream-Based Processing
NodeStream enables declarative, pipeline-style processing on OBNT.
List<String> tags = NodeStream.of(node)
.findByPath("$.tags[*]", String.class)
.filter(t -> t.length() > 3) // Programmatic filtering
.toList();Multi-Stage Evaluation
Each stage treats the previous stage’s result as the new root.
int x = jo.stream()
.findByPath("$..profile", JsonObject.class) // Primary
.filter(n -> n.hasNonNull("values"))
.getByPath("$.x", Integer.class) // Secondary
.findFirst()
.orElse(4);Programmatic Aggregation
double avgScore = jo.stream()
.find("$.scores[*]", Double.class)
.map(d -> d < 60 ? 60 : d) // Custom normalization
.collect(Collectors.averagingDouble(s -> s));Why Path in OBNT
SJF4J applies path navigation directly on plain Java objects (OBNT), instead of operating on a separate JSON AST.
This means:
- The same path engine works across
Map,List,POJO, andJOJO - No intermediate tree conversion is required
- Mutations apply to the actual object graph
- Path evaluation integrates naturally with Java Streams
In SJF4J, JsonPath is part of the core structural model, not an external query layer.