Description
When an object contains an empty-string key ("") whose value is an object or array, encode() omits the "": header line entirely and emits the children at the parent's indentation level. No error or warning is raised. This is silent data loss, and depending on the surrounding structure it can also overwrite sibling keys or change the document's root type. Empty keys with primitive values encode correctly ("": 1), so the bug is specific to the non-primitive branch.
The decoder is not at fault: given correct wire text ('"":\n "a": 1'), decode() returns the right value. The reference TypeScript implementation (2.3.0) and toon-rust (0.5.0) both encode these inputs correctly, so Python-encoded output misdecodes in every implementation — including toon-python itself.
Reproduction Steps
from toon_format import encode, decode
encode({"": {"a": 1}}) # 'a: 1' — empty-key level silently deleted
encode({"": {"a": 1}, "a": 2}) # 'a: 1\na: 2' — duplicate key; decodes to {"a": 2}
encode({"": [1, 2]}) # '[2]: 1,2' — root object became a root array
decode(encode({"": {"a": 1}})) # {"a": 1} — round-trip silently loses structure
decode('"":\n "a": 1') # {"": {"a": 1}} — decoder handles correct wire fine
Expected Behavior
Per SPEC.md §7.3, keys not matching ^[A-Za-z_][A-Za-z0-9_.]*$ must be quoted (the empty string does not match), and per §8, a nested object is emitted as key: on its own line with fields at depth +1. So {"": {"a": 1}} should encode as:
This is exactly what @toon-format/toon 2.3.0 and toon-rust 0.5.0 produce, and what toon-python's own decoder round-trips correctly.
Actual Behavior
The "": header is never emitted; children are promoted one level up. Observed consequences:
- Silent structural data loss on round-trip:
{"": {"a": 1}} → {"a": 1}
- Sibling-key collision where the last duplicate wins:
{"": {"a": 1}, "a": 2} → wire a: 1\na: 2 → {"a": 2}
- Root-type change from object to array:
{"": [1, 2]} → wire [2]: 1,2 → [1, 2]
- With an array value nested deeper, wire that decodes to garbage:
{"a": {"": [1, 2]}} → {"a": {"[2]": "1,2"}}
Environment
toon_format 0.9.0b1, Python 3.14, macOS. Cross-checked against @toon-format/toon 2.3.0 (Node 24) and toon-rust 0.5.0.
Additional Context
Found by differential fuzzing with toon-diff, which compares TS/Python/Rust round-trips against an independent oracle. Related: the TS implementation fixed an adjacent empty-key issue in array headers in toon#281, suggesting empty-string keys are a known-delicate area. Distinct from #63, which concerns empty-object values in tabular cells; this issue concerns empty-string keys in plain object nesting.
Description
When an object contains an empty-string key (
"") whose value is an object or array,encode()omits the"":header line entirely and emits the children at the parent's indentation level. No error or warning is raised. This is silent data loss, and depending on the surrounding structure it can also overwrite sibling keys or change the document's root type. Empty keys with primitive values encode correctly ("": 1), so the bug is specific to the non-primitive branch.The decoder is not at fault: given correct wire text (
'"":\n "a": 1'),decode()returns the right value. The reference TypeScript implementation (2.3.0) and toon-rust (0.5.0) both encode these inputs correctly, so Python-encoded output misdecodes in every implementation — including toon-python itself.Reproduction Steps
Expected Behavior
Per SPEC.md §7.3, keys not matching
^[A-Za-z_][A-Za-z0-9_.]*$must be quoted (the empty string does not match), and per §8, a nested object is emitted askey:on its own line with fields at depth +1. So{"": {"a": 1}}should encode as:This is exactly what @toon-format/toon 2.3.0 and toon-rust 0.5.0 produce, and what toon-python's own decoder round-trips correctly.
Actual Behavior
The
"":header is never emitted; children are promoted one level up. Observed consequences:{"": {"a": 1}}→{"a": 1}{"": {"a": 1}, "a": 2}→ wirea: 1\na: 2→{"a": 2}{"": [1, 2]}→ wire[2]: 1,2→[1, 2]{"a": {"": [1, 2]}}→{"a": {"[2]": "1,2"}}Environment
toon_format 0.9.0b1, Python 3.14, macOS. Cross-checked against @toon-format/toon 2.3.0 (Node 24) and toon-rust 0.5.0.
Additional Context
Found by differential fuzzing with toon-diff, which compares TS/Python/Rust round-trips against an independent oracle. Related: the TS implementation fixed an adjacent empty-key issue in array headers in toon#281, suggesting empty-string keys are a known-delicate area. Distinct from #63, which concerns empty-object values in tabular cells; this issue concerns empty-string keys in plain object nesting.