Working with the data model
A surface's data lives in a plain object, and you read and write it through three pointer helpers plus a resolver. This guide covers all four.
The data model is just a Record<string, unknown>. A2UI components reference into it by JSON-Pointer-style path; @threadplane/a2ui gives you the helpers to read, write, and resolve against it.
#The pointer helpers
getByPointer, setByPointer, and deleteByPointer all take a model and a pointer string (/user/name, /items/1).
getByPointer walks the path and returns the value, or undefined if any segment is missing:
setByPointer writes immutably. It returns a clone with the change applied; the original is untouched.
It also creates intermediate objects along the way, so you don't have to pre-build nesting:
deleteByPointer removes a key, again immutably:
If the parent of the target doesn't exist, deleteByPointer returns the original model unchanged rather than fabricating a path to delete from.
These helpers use JSON-Pointer-style syntax but do not implement RFC 6901's ~0 / ~1 unescaping. A path is split on / and the segments are used as literal keys. So keys that themselves contain / or ~ aren't addressable — there's no escape sequence to reach them.
#Resolving dynamic values
resolveDynamic collapses a component's prop to a concrete value against the model. The order is fixed:
null/undefinedpass through as-is.- Arrays are mapped recursively — each element resolved in turn.
- Literal wrappers unwrap first:
literalString,literalNumber,literalBoolean,literalArray. - A
{ path }reference reads from the model. - Anything else — a plain string, number, or unrecognized object — passes through unchanged.
A missing path resolves to undefined, never an error. That keeps a half-streamed surface renderable while data is still arriving.
#Scopes and template children
How do you resolve a relative path, like inside a repeated template row?
resolveDynamic takes an optional third argument, an A2uiScope:
Path resolution depends on the leading slash:
- An absolute path (
/name) always resolves from the model root, scope or not. - A relative path (
name) resolves againstscope.basePathwhen a scope is given.
With basePath: '', the relative path name resolves to /name. That's the lever the template / dataBinding pattern pulls. When a container repeats a template over the array at, say, /items, it resolves each instance's props with a per-item scope:
A2uiScope carries an item field, but the resolver only reads basePath to rewrite relative paths. item is typed for callers that want the bound element on hand, yet resolveDynamic itself never touches it. Don't expect setting item to change resolution.
#Next
- Validating and adapting an A2UI stream — guards, test payloads, and wiring a custom renderer.