Tree Model and RFC 6901 JSON-Pointer
The MarshalledNode class is a typed tree façade layered over the generic-collections model — MarshalledMap / MarshalledList and their JsonMap / JsonList subclasses.
It gives developers coming from Jackson the ergonomics they expect — node-type introspection, typed value accessors, fluent tree building, and RFC 6901 JSON-Pointer addressing — without introducing a parallel Jackson-style node hierarchy.
MarshalledNode is a live view, not a copy and not a new node type. A node wraps a single backing value that is one of:
- a
Map(an object node), - a
List(an array node), - a scalar (
String,Number,Boolean, …) (a value node), or null(a null node).
The underlying representation is still plain MarshalledMap/MarshalledList (or any Map/List). There is no ObjectNode /
ArrayNode / TextNode class tree to learn — MarshalledNode is the single façade type for every node kind.
Creating nodes
Use the static factories to wrap an existing value or to start a fresh tree:
// Wrap an existing value (stored as-is, no copy).
MarshalledNode node = MarshalledNode.of(myJsonMap);
// Start fresh containers (backed by a new empty JsonMap / JsonList).
MarshalledNode obj = MarshalledNode.objectNode();
MarshalledNode arr = MarshalledNode.arrayNode();
You can also obtain a node directly from the collections classes via the convenience MarshalledMap.at(String)
and MarshalledList.at(String) methods, which
resolve an RFC 6901 pointer and return a MarshalledNode:
JsonMap doc = Json.to("{foo:{bar:['x','y']}}", JsonMap.class);
// Returns a MarshalledNode wrapping "y".
MarshalledNode n = doc.at("/foo/bar/1");
Node-type introspection
Four predicates report the kind of value a node wraps:
Map.List.null scalar.null.Typed accessors
Value nodes can be read in a target type. The scalar accessors return null when the node is not a value node or the value
cannot be converted (rather than throwing):
String s = node.get("name").asString();
Integer i = node.get("age").asInt();
Long l = node.get("id").asLong();
Double d = node.get("score").asDouble();
Boolean b = node.get("active").asBoolean();
For richer conversions — including converting an object node straight to a bean — use as(Class),
which routes through the same conversion machinery as MarshalledMap.get(String,Class). Use value()
to get the raw backing object:
// Convert an object node to a bean.
Address addr = node.get("address").as(Address.class);
// Get the raw backing value.
Object raw = node.value();
Navigation
Navigate into children by key (object nodes) or index (array nodes). Both get(...) forms return null on a miss (absent
key, out-of-range index, or wrong node kind), and size()
returns the entry/element count (0 for value and null nodes):
MarshalledNode child = node.get("address"); // by key
MarshalledNode first = node.get("phones").get(0); // by index
int count = node.get("phones").size();
Fluent tree building
The put(String,Object)
(object nodes) and add(Object)
(array nodes) builders return this for chaining. They mutate the backing container in place, so callers still holding the
underlying map/list see the change.
// Build a tree fluently (mutates a backing JsonMap/JsonList in place).
MarshalledNode node = MarshalledNode.objectNode()
.put("a", 1)
.put("b", MarshalledNode.arrayNode().add("x").add("y").value());
String json = Json.of(node.value()); // {"a":1,"b":["x","y"]}
// Navigate and read typed values.
Integer a = node.get("a").asInt(); // 1
String x = node.get("b").get(0).asString(); // "x"
put(...) on a non-object node and add(...) on a non-array node both throw IllegalStateException.
Live view vs. copy() snapshot
Because a node is a live view, building or mutating through it changes the shared backing container:
JsonMap backing = new JsonMap();
MarshalledNode node = MarshalledNode.of(backing);
node.put("x", 1);
// backing now contains {"x":1} — the caller's map saw the mutation.
When you need an independent snapshot, use copy(),
which returns a new node backed by a recursive deep copy (maps into new JsonMaps, lists into new JsonLists; scalars and
null are returned as-is). Mutating the copy never affects the original and vice versa:
MarshalledNode snapshot = node.copy();
snapshot.put("x", 999); // does not affect the original 'node'.
RFC 6901 JSON-Pointer addressing
MarshalledNode exposes the full RFC 6901 pointer surface relative to its
backing value:
null on a read-miss.Optional<MarshalledNode> (distinguishes a present null value from an absent one).- token appends to a list. The root pointer is rejected.null if absent).JsonMap doc = Json.to("{foo:{bar:['x','y']}}", JsonMap.class);
MarshalledNode root = MarshalledNode.of(doc);
// Read.
String y = root.at("/foo/bar/1").asString(); // "y"
// Optional read — present even when the value is JSON null.
Optional<MarshalledNode> maybe = root.find("/foo/baz"); // Optional.empty()
// Auto-vivifying write — intermediate containers are created as needed.
root.set("/foo/qux/0", "new"); // creates {foo:{qux:["new"]}} alongside bar
// Append to a list with the '-' token.
root.set("/foo/bar/-", "z"); // bar is now ['x','y','z']
// Remove.
Object removed = root.remove("/foo/bar/0"); // returns "x"
A read-miss (missing key, out-of-range index, or type mismatch) resolves to null from at(...). The empty pointer ""
addresses the whole backing document; on a node, set("", …) is rejected with IllegalArgumentException because the backing
value cannot be replaced in place — set against a non-empty pointer instead.
The JsonPointer helper
For pointer operations against an arbitrary Map/List root (not just through a node), use the standalone
JsonPointer class directly. A pointer is a string of
zero or more /-prefixed reference tokens; the empty string "" references the whole document (the root), and a non-empty
pointer must begin with /.
JsonMap doc = JsonMap.of("foo", JsonList.of("bar", "baz"));
// Read (read-miss resolves to null).
Object v = JsonPointer.of("/foo/0").eval(doc); // "bar"
// Auto-vivifying write; '-' appends. Returns the (possibly new) effective root.
JsonPointer.of("/foo/-").set(doc, "qux"); // appends "qux"
// Remove returns the removed value (null if absent).
Object removed = JsonPointer.of("/foo/0").remove(doc); // "bar"
Token escaping (~0 / ~1) and the - append token
Within a reference token, / is escaped as ~1 and ~ is escaped as ~0 (decode order matters: ~1→/ first, then
~0→~). Use encodeToken(String)
and decodeToken(String)
to convert between raw member names and escaped tokens:
// A member name literally named "a/b~c".
JsonPointer.encodeToken("a/b~c"); // "a~1b~0c"
JsonPointer.decodeToken("a~1b~0c"); // "a/b~c"
// Addressing that member:
JsonMap doc = Json.to("{'a/b~c':42}", JsonMap.class);
Object v = JsonPointer.of("/a~1b~0c").eval(doc); // 42
On a write, the special - token references "the element after the last element" of an array — i.e. it appends:
JsonMap doc = Json.to("{items:[1,2]}", JsonMap.class);
JsonPointer.of("/items/-").set(doc, 3); // items is now [1,2,3]
toString() round-trips a parsed pointer back to its canonical escaped string form. Array index tokens are strict base-10
non-negative integers with no leading zeros (0 itself is allowed); anything else is treated as an object member name.
MarshalledNode and JsonPointer are part of the next-generation typed tree model layered over the Marshalled* collections
and are marked Beta — API subject to change. The underlying MarshalledMap/MarshalledList path-navigation methods
(getAt/putAt/postAt/deleteAt) remain the stable, long-standing way to address nodes by slash-delimited path.