A thin wrapper around Jackson's JsonNode that carries two pieces of provenance: its JsonPointer (absolute location within the document) and a reference to the document's root node. It supports the full set of Jackson navigation methods (get, path, at by name, index, or JsonPointer), upward navigation via parent(), and forward search via JSONPath (RFC 9535). Every navigation call propagates provenance automatically, so you always know where a value came from and can navigate back to the full document from any node.
When you traverse a JSON document with plain JsonNode, you lose location information. Error messages become vague ("invalid value") and diagnostics require re-traversal. TrackedJsonNode keeps the pointer, the value, and the document root together at all times — no re-threading of context through your call stack.
- Java 21
- Jackson Databind 2.18+
| Package | Contents |
|---|---|
org.json_kula.tracked_json.json_node |
TrackedJsonNode — the core pointer-tracking wrapper |
org.json_kula.tracked_json.json_pointer |
JsonPointerStep — last-segment descriptor |
org.json_kula.tracked_json.json_path |
JsonPathSearch, JsonPathExpression, InvalidPathException — JSONPath public API |
org.json_kula.tracked_json.json_patch |
JsonPatch, JsonPatchException — JSON Patch public API |
<dependency>
<groupId>io.github.vlad-public-code</groupId>
<artifactId>tracked-json</artifactId>
<version>1.0.0</version>
</dependency>ObjectMapper mapper = new ObjectMapper();
JsonNode doc = mapper.readTree(json);
TrackedJsonNode root = TrackedJsonNode.ofRoot(doc);
// get / path — field and index navigation
TrackedJsonNode price = root.get("order").get("items").get(0).get("price");
System.out.println(price.asDouble()); // 9.99
System.out.println(price.pointer().toString()); // /order/items/0/price
// at — jump directly by JsonPointer
TrackedJsonNode same = root.at("/order/items/0/price");
System.out.println(same.pointer().toString()); // /order/items/0/price
// parent — navigate upward
TrackedJsonNode item = price.parent();
System.out.println(item.pointer().toString()); // /order/items/0
// JsonPath — search the document
List<TrackedJsonNode> matches = JsonPathSearch.find(root, "$.order.items[?(@.price < 10)]");
matches.forEach(n -> System.out.println(n.pointer() + " = " + n));TrackedJsonNode.ofRoot(JsonNode root) // pointer = ""
TrackedJsonNode.of(JsonNode root, JsonNode node, JsonPointer p) // arbitrary position| Method | Description | Missing behaviour |
|---|---|---|
get(String field) |
Navigate to an object property by name | returns null |
get(int index) |
Navigate to an array element by zero-based index | returns null |
path(String field) |
Navigate to an object property by name (null-safe) | returns MissingNode-wrapped node |
path(int index) |
Navigate to an array element by zero-based index (null-safe) | returns MissingNode-wrapped node |
at(JsonPointer rel) |
Navigate to a descendant by relative JSON Pointer | returns MissingNode-wrapped node |
at(String jsonPtrExpr) |
Navigate to a descendant by relative JSON Pointer expression | returns MissingNode-wrapped node |
parent() |
Navigate to the parent node | returns MissingNode-wrapped node for the root |
Field names containing / or ~ are RFC 6901-escaped in the pointer automatically.
// Array elements or object values — each wrapped with its pointer
Iterator<TrackedJsonNode> values()
Stream<TrackedJsonNode> valueStream()
// Object properties
Stream<Map.Entry<String, TrackedJsonNode>> propertyStream()
Set<Map.Entry<String, TrackedJsonNode>> properties()
void forEachEntry(BiConsumer<String, TrackedJsonNode> action)Implements RFC 9535 — JSONPath: Query Expressions for JSON.
// Compile once, evaluate many times
JsonPathExpression expr = JsonPathExpression.compile("$.store.book[*].author");
List<TrackedJsonNode> results = JsonPathSearch.find(root, expr);
// Or parse and evaluate in one call
List<TrackedJsonNode> results = JsonPathSearch.find(root, "$.store.book[?(@.price < 10)]");
// Each result carries its absolute pointer
results.forEach(n -> System.out.println(n.pointer() + " = " + n.asText()));Supported features:
- Child (
$.name,$['name']) and wildcard (.*,[*]) selectors - Index (
[0],[-1]) and slice ([1:5:2]) selectors - Recursive descent (
..) - Union (
['a','b'],[0,2]) - Filter expressions (
[?(...)]) with&&,||,!, comparison operators, and nested paths - Function extensions:
length(),count(),value(),match(),search()(I-Regexp per RFC 9485)
Throws InvalidPathException for malformed expressions; returns an empty list when nothing matches. Validated against the RFC 9535 Compliance Test Suite (703 tests).
Implements RFC 6902 — JavaScript Object Notation (JSON) Patch. Supports all six operations: add, remove, replace, move, copy, test.
// Compile once (validates the patch document), apply many times
JsonNode patch = mapper.readTree("""
[
{ "op": "replace", "path": "/status", "value": "shipped" },
{ "op": "add", "path": "/updatedAt", "value": "2025-01-01" },
{ "op": "test", "path": "/version", "value": 3 }
]
""");
JsonPatch compiled = JsonPatch.compile(patch); // throws JsonPatchException if invalid
JsonNode result = compiled.apply(document); // throws JsonPatchException if an operation fails
// Convenience overload for TrackedJsonNode — returns a new root-tracked node
TrackedJsonNode result = compiled.apply(trackedDocument);compile validates the structure of every operation (missing fields, unknown ops, non-pointer path/from values). apply works on a deep copy of the document, so the original is never mutated even when a later operation fails.
JsonPointerStep step = node.step();
switch (step) {
case JsonPointerStep.Name(var name) -> System.out.println("field: " + name);
case JsonPointerStep.Index(var idx) -> System.out.println("index: " + idx);
}JsonPointerStep.ROOT (Name("")) is returned for the root node.
JsonNode node() // raw Jackson node
JsonPointer pointer() // absolute path from document root
JsonNode root() // document root node
// Type checks: isObject, isArray, isValueNode, isMissingNode, isNull,
// isTextual, isNumber, isBoolean, has(String), has(int), size()
// Value reads: asText, asInt, asLong, asDouble, asBooleanmvn test