Map Methods
CXL provides six built-in methods for working with map values (key-value pairs). Maps arise naturally from JSON object inputs, from the set builder below, and from upstream emits that produce nested structures.
All map methods return new values – they never mutate the receiver. This is copy-on-write semantics: chaining .set then .remove_field produces a fresh map at each step, leaving the upstream binding untouched.
Null propagation
Every map method returns null when the receiver is null or is not a Value::Map.
Method reference
keys() -> Array
Returns the map’s keys as an array of strings, preserving insertion order.
- type: transform
name: list_keys
input: rows
config:
cxl: |
emit field_names = profile.keys()
For an input record where profile is {"name":"Alice","tier":"gold","since":"2021-04"}, field_names is ["name","tier","since"].
values() -> Array
Returns the map’s values as an array, preserving insertion order. Value types are heterogeneous – the array carries each value as-is.
cxl: |
emit field_values = profile.values()
field_values is ["Alice","gold","2021-04"].
merge(other: Map) -> Map
Returns a new map containing every key from the receiver and from other. On conflicting keys, other’s value wins.
cxl: |
emit enriched = profile.merge(overrides)
For profile = {"name":"Alice","tier":"gold"} and overrides = {"tier":"platinum","since":"2021-04"}, enriched is {"name":"Alice","tier":"platinum","since":"2021-04"}.
set(key: String, value: Any) -> Map
Returns a new map with key set to value. If the key was already present, its value is replaced; insertion order is preserved.
cxl: |
emit stamped = profile.set("region", "us-east")
stamped is {"name":"Alice","tier":"gold","since":"2021-04","region":"us-east"}.
Nested paths
key may be a dotted/indexed path that descends into nested maps and arrays, so a single set writes into a deep document. Dots separate map keys; a [n] suffix indexes an array.
cxl: |
emit moved = profile.set("address.city", "NYC")
emit relabel = order.set("items[0].sku", "A-100")
- Auto-create. Missing intermediate map segments are created as empty maps, so a path can build structure that does not yet exist.
{}.set("a.b.c", 7)returns{"a":{"b":{"c":7}}}. This is what letssetassemble a nested document from scratch (matching jqsetpathand Bloblang assignment). - Type conflict -> null. If an intermediate segment already exists but is the wrong kind for the next step – descending into a key whose value is a scalar, indexing a map with
[n], or naming a field on an array – the whole operation returnsnull. Nothing is partially written. - Array index past the end -> null. Indexing past the last element returns
nullfor the whole operation; arrays are never silently grown. The path can only overwrite an array slot that already exists. - A bare key is a single key, not a path.
"region"writes the top-levelregion. Only.and[n]introduce nesting; a key with neither behaves exactly as before.
For profile = {"name":"Alice","address":{"city":"LA"}}, profile.set("address.city", "NYC") is {"name":"Alice","address":{"city":"NYC"}} – the sibling name and any other address keys are preserved.
Known limitation. Because
.and[are path syntax,setcannot yet target a key whose name literally contains a.or[(for example a JSON field literally named"a.b"). Column names solve this with a backslash escape —a\.bis one segment nameda.b, per Field Paths — and the decided direction is forset/unsetkeys to read that same grammar, since both are flat strings addressing a path. Until they do: to write such a key, build it withmergeand a map literal; to remove it, useremove_field, which matches the exact key string.
remove_field(key: String) -> Map
Returns a new map without key. If the key was absent, the receiver is returned unchanged.
cxl: |
emit slim = profile.remove_field("since")
slim is {"name":"Alice","tier":"gold"}.
unset(key: String) -> Map
Returns a new map with the entry addressed by key removed. unset is the deletion counterpart to set and reuses the same dotted/indexed path grammar: dots separate map keys, a [n] suffix indexes an array. A bare key (no . or [n]) drops a top-level entry, exactly like remove_field.
cxl: |
emit pruned = profile.unset("address.city")
emit dropped = order.unset("items[0]")
- Array element removes and shifts.
unset("items[0]")deletes element 0 and shifts the remaining elements down, so the array shrinks by one (matching jqdel). This is deliberately distinct fromset("items[0]", null), which leaves anullhole —unsetmeans delete. - Missing or conflicting path is a no-op. A path that does not resolve — a missing intermediate, a missing final key, an array index past the end, or a type conflict (a field segment against an array, an index segment against a map) — returns the receiver unchanged. This mirrors
remove_fieldon an absent key, and is the opposite ofset, which returnsnullon a conflicting path. - Copy-on-write. Like every map method,
unsetnever mutates the receiver; the upstream binding is untouched.
For profile = {"name":"Alice","address":{"city":"LA","zip":"90001"}}, profile.unset("address.city") is {"name":"Alice","address":{"zip":"90001"}} – the sibling zip and the top-level name are preserved.
Worked example: chained set + remove_field
Map methods compose naturally because each returns a new map.
- type: transform
name: rewrite_profile
input: rows
config:
cxl: |
emit profile =
profile.set("region", "us-east").remove_field("internal_id")
For profile = {"name":"Alice","internal_id":"ix-77","tier":"gold"}, the emitted profile is {"name":"Alice","tier":"gold","region":"us-east"}. The internal_id slot is removed and the region slot is appended; both happen on a fresh map so the upstream record’s profile is unaffected for any other downstream branch.
Parentheses are required
All map methods are method calls and must be written with parentheses, even the zero-argument ones:
profile.keys() -- ok
profile.keys -- parses as a field lookup, not a method call
profile.keys parses as a dotted path – a lookup for a field literally named keys inside profile. That path almost certainly returns null. Always include the parentheses when invoking a map method.
Using map methods inside array closures
Map methods compose with closure-bearing array builtins when the array elements are themselves maps.
cxl: |
emit enriched_items = items.map(it => it.set("region", "us-east"))
emit item_keys = items.map(it => it.keys())
Each it is a map; the closure body invokes a map method on it. enriched_items is an array where every element gained a region field. item_keys is an array of key-name arrays, one per element.
See also
- Closures – arrow-syntax closures often invoke map methods on their
itbinding. - Array Methods – closure-bearing array methods commonly carry maps as their elements.
- Nested Paths – bracket-index access (
profile["name"]) reads a single key without producing a new map.