Keyboard shortcuts

Press or to navigate between chapters

Press S or / to search in the book

Press ? to show this help

Press Esc to hide this help

Field Paths

A column name is not opaque. An unescaped . inside it separates path segments, so the column Address.City addresses the path AddressCity. Readers produce such names when they flatten nested input, writers expand them back into nested output, and the rule for reading them is the same everywhere in Clinker — one grammar, not one per format.

This page is the reference for that grammar. The places it applies:

  • Column names declared in a source or output schema:.
  • Column names a self-describing reader infers (the JSON reader flattens {"a":{"b":1}} to the column a.b; the XML reader flattens <a><b>1</b></a> the same way).
  • Column names a writer expands back into nesting — see Writing JSON and Writing XML.

It does not cover CXL expressions. Reaching into a value — a map or array held in one column — uses the expression forms on Nested Paths (profile.city, profile["a.b"], items[0]). The two are different surfaces over the same idea: a path is an ordered list of segments either way, but a column name spells it with . and a backslash escape, while an expression spells it with dots and brackets.

The rule

Reading a name left to right:

InputMeaning
.Ends the current segment, starts the next.
\.A literal . inside the current segment.
\\A literal \.
\[A literal [.
\ before anything elseAn error. \], \t, and a name ending in \ are all rejected.
Any other characterA literal character of the current segment — including a bare [, and ], @, $, /, and whitespace.

So Address.City is two segments, and a\.b is one segment named a.b.

Three consequences worth stating outright:

  • An unrecognized escape is an error, never a literal. A \ must be followed by ., [, or \ — nothing else, and not the end of the name. C:\temp is rejected, because silently treating \t as the two characters \ and t would make the encoding ambiguous, and silently dropping the \ would rename the column without saying so. Write C:\\temp. The same applies to \], which is covered below.
  • Empty segments are real. a..b is three segments — a, the empty name, and b. An empty key is a value a document can genuinely carry, so the grammar does not reject it.
  • A name may nest at most 64 levels deep. This matches the depth at which the JSON reader stops flattening, so any name that reader produces is a name a writer can expand. The XML reader flattens with no depth bound, so an extraordinarily deep XML document can produce a name past the cap; writing it back out fails with a clear error rather than recursing without limit.

Writing a literal bracket

[ is currently a literal character, so a column named a[0] works. It is nonetheless reserved: bracket indexing may later be given meaning inside a flat name, matching the [n] form CXL expressions already use. Writing \[ means “a literal [” today and will keep meaning exactly that. A bare [ is not guaranteed to.

So the column a[0] future-proofs as a\[0] — escaping only the opening bracket:

SpellingResult
a\[0]One segment named a[0]. Correct, and stable across the reserved-[ change.
a\[0\]Rejected\] is not an escape.
a[0]One segment named a[0] today, but a bare [ is the form that is not guaranteed to keep that meaning.

Only the opening bracket is ever escaped. ] is never escaped and never needs to be: it would carry meaning only as the close of an unescaped [, so a literal ] standing on its own is already unambiguous. Escaping it would add noise without removing any ambiguity.

Writing \] is an error rather than a silently-accepted no-op, for the same reason C:\temp is: an escape that quietly meant nothing would let two different names decode to the same path, and one of them would be dropped. The error names the offending escape and the rule.

If a column name of yours contains [, escaping it now costs nothing and makes it future-proof.

Expansion on write

A writer that can express nesting rebuilds it from the decoded paths.

Grouping. Columns sharing a prefix collect into one container, positioned where that prefix first appeared — even when the schema interleaves them. Columns Address.City, name, Address.State write as:

{"Address":{"City":"Boston","State":"MA"},"name":"Ada"}

Absent children. A column omitted for a record (a null under preserve_nulls: false) contributes nothing, and a container whose every descendant is omitted emits no key at all rather than an empty object. That keeps the round trip honest: reading {"a":{}} produces no column, so writing no column must produce no "a".

Values are untouched. Expansion adds structure above a column’s value; a Value::Map or array held in that column still serializes as itself. A column Items.Item holding [1,2] writes as {"Items":{"Item":[1,2]}}.

No carve-outs. The rule reads the column-name string and nothing else, so it applies identically to engine-stamped columns. Under include_correlation_keys: true the column $ck.customer_id writes as {"$ck":{"customer_id":…}}.

When two names clash

Two columns can describe places that cannot both exist. The writer refuses the whole column set before writing a single byte, naming both columns — it never silently keeps one.

ColumnsWhy
a and a.ba holds a value and is also the container a.b sits inside.
a.b and a.b.cThe same clash, one level down.
a[b and a\[bTwo spellings of the identical path.

a.b and a\.b do not clash: they address ab and the single segment a.b. That is what the escape is for.

When a clash comes from a . you meant literally, the diagnostic offers the escaped spelling:

XML writer cannot expand this output's column names into nested output: field
names `a.b` and `a.b.c` cannot both be written: `a.b` holds a value and is also
the container `a.b.c` nests inside. Rename one of them, or — if the `.` in `a.b`
is part of the name rather than a nesting separator — declare it as `a\.b`.

Where the grammar does not reach yet

Two surfaces read column names without this grammar. Both are tracked, and both are stated here so the current behavior is a known position rather than a surprise:

  • Flat writers emit the raw name. CSV and fixed-width have no nesting to expand into, so they write the column name verbatim — a column declared a\.b appears as the literal CSV header a\.b, backslash included. Whether flat writers should emit the decoded name instead is a separate decision.
  • Readers join without escaping. The JSON and XML readers join flattened path segments with a plain ., so a source key that literally contains a . ({"a.b": 1}) arrives as the column a.b — indistinguishable from a nested {"a":{"b":1}}, and it writes back nested. Closing that means escaping each key as the reader joins it, tracked by issue 920.

set and unset in CXL also use a path grammar of their own that does not yet support escaping — see the known limitation on Map Methods.

See also

  • Nested Paths — the expression-side forms for reaching into a value.
  • Writing JSON — expansion on the JSON write side.
  • Writing XML — expansion on the XML write side, plus the attribute convention layered over it.