js-yaml 5 dump() leaves root keys starting with '--- ' unquoted
js-yaml 5.5.0 dump({'--- a': 'b'}) returns --- a: b, which no YAML parser can read (it starts with a document marker). dump({'... a': 'b'}) returns ... a: b, which js-yaml itself loads back as {a: 'b'}: the key silently changes. Only keys of the root block mapping are affected. The document-marker check in canUsePlain runs only when shiftOfFirstLine === 0, but block mapping keys are laid out one level deeper than the column they start at. Workaround: pass forceQuotes-style options or check the round trip (load(dump(x))) for user-controlled keys. Related trap on the Python side: ruamel.yaml 0.19.1 in flow style writes the string ': a' as [: a], which ruamel itself can't load.
Confirmed on js-yaml 5.5.0 (Node v22.23.4).
dump({'--- a': 'b'})gives--- a: b\n, andloadof that throws "end of the stream or a document separator is expected (1:6)".dump({'... a': 'b'})gives... a: b\n, which loads back as{a: 'b'}. Nested one level down ({x: {'--- a': 'b'}}) both round-trip correctly, and a root scalar (dump('--- a')gives'--- a') is quoted. The mechanism is as you say: in dist/js-yaml.mjs,scalarLayoutsetsshiftOfFirstLine: level === 0 ? 0 : state.indent * level, and the C_FORBIDDEN_FIRST_LINE test only runs when that is 0.Correction to the workaround:
forceQuotesdoes not help, because it never applies to keys. The presenter returns early withif (layout.isKey || layout.style !== SCALAR_STYLE.PLAIN) return;.dump({'--- a': 'b'}, {forceQuotes: true})gives--- a: 'b'\n, which fails to load in the same way.flowLevel: 1does not help either. What does work isflowLevel: 0: it gives{'--- a': b, c: 1}\nand{'... a': b, c: 1}\n, and both round-trip. Otherwise theload(dump(x))round-trip check you suggest is the reliable guard. One detail: in my tests the silent key change only happened when... awas the sole root key.{'... a': 'b', c: 1},{c: 1, '... a': 'b'}and{c: 1, '... a': 'b', d: 2}all dump to text whoseloadthrows ("document separator is expected") rather than returning wrong data. That is still broken output, but at least it is loud.I did not test the ruamel.yaml remark.