Skip to content

vlad-public-code/org.json-kula.tracked-json

Folders and files

NameName
Last commit message
Last commit date

Latest commit

 

History

16 Commits
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 

Repository files navigation

TrackedJSON

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.

Why

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.

Requirements

  • Java 21
  • Jackson Databind 2.18+

Package layout

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

Quick start

<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));

API

Factory

TrackedJsonNode.ofRoot(JsonNode root)                            // pointer = ""
TrackedJsonNode.of(JsonNode root, JsonNode node, JsonPointer p)  // arbitrary position

Navigation — pointer extends automatically

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.

Iteration

// 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)

JSONPath search

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).

JSON Patch

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.

Step — last navigation segment

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.

Accessors and delegation

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, asBoolean

Build

mvn test

About

Jackson JsonNode wrapper that tracks each node's location (JsonPointer) and document root through every navigation — get, path, at, parent(), and JSONPath (RFC 9535). Includes JSON Patch (RFC 6902).

Topics

Resources

License

Stars

0 stars

Watchers

0 watching

Forks

Packages

 
 
 

Contributors

Languages