Skip to content

feat(yaml/unstable): add useMaps option to parse mappings into Maps - #7290

Open
tomas-zijdemans wants to merge 5 commits into
denoland:mainfrom
tomas-zijdemans:yaml-usemaps
Open

feat(yaml/unstable): add useMaps option to parse mappings into Maps#7290
tomas-zijdemans wants to merge 5 commits into
denoland:mainfrom
tomas-zijdemans:yaml-usemaps

Conversation

@tomas-zijdemans

Copy link
Copy Markdown
Contributor

Adds an opt-in useMaps boolean to the unstable parse()/parseAll(): mappings come back as Maps at every depth, and keys keep their parsed types instead of being coerced to strings. 3: is the number 3, "3": is the string "3". Closes #7283.

The unstable stringify() learns to write Map and Set intrinsically, with no new option: a Map has exactly one sensible representation (a mapping in insertion order), so stringify(parse(src, { useMaps: true })) round-trips. Stable parse() and stringify() are byte-for-byte unchanged, pinned by the existing tests plus a new one asserting stringify(new Map(...)) still yields {}\n.

What changed

  • useMaps?: boolean (default false) on the unstable ParseOptions, with the full contract in TSDoc. Keys resolve through the schema exactly like values, complex keys (sequences, mappings) are legal, and the "[object Object]" substitution plus the "nested arrays are not supported inside keys" error do not apply in this mode.
  • !!set constructs a Set, !!omap a Map, !!pairs an array of [key, value] pairs under the flag. These ride along because their constructs are Object.keys-based and would break on Map input.
  • Map stringify: scalar keys inline (3: A3), collection keys in explicit ? key form on their own lines. Sets get the !!set tag. Both participate in duplicate detection and &ref_N anchors, including objects used as keys.

Decisions worth reviewing

  • The return type stays unknown. A conditional type keyed on the option lies as soon as extraTypes or the extended schema is in play.
  • Duplicate keys compare SameValueZero, like Map keys, so 3 and "3" coexist. Structurally equal complex keys are distinct unless they are the same aliased node. Deep-equality hashing is a hole with no bottom, so I stayed out of it.
  • The Map is allocated exactly where {} is allocated today, before any pair lands, so anchors, aliases, and cycles need no new mechanism. Converting after the fact in construct() cannot work: by then keys are strings and aliases point at the old object.
  • Internally the loader forks on a private container protocol (create/has/set/entries) with a plain-object adapter and a Map adapter. The legacy adapter keeps the direct-assignment fast path from perf(yaml): use direct assignment for mapping keys #7126 and the __proto__ armor. If a public tag seam ever becomes worth it, this makes it an unhiding rather than a rewrite.
  • sortKeys does not apply to Map entries (insertion order is the contract, and the callback is string-typed). Documented in one sentence.

One known wart: !!set with a null body (--- !!set, no entries) still constructs {} under the flag, because tag constructs never see parser options. An empty flow mapping (!!set {}) constructs an empty Set correctly.

Testing

Fifteen new tests across parse_test.ts and stringify_test.ts, covering the issue's exact YAML, 2: vs "2":, complex and aliased keys, duplicate detection with and without allowDuplicateKeys, merge keys, cycles, __proto__ as an inert key, set/omap/pairs, explicit-key stringify, anchors, condenseFlow, and full round-trips. All 111 yaml tests pass, along with deno task lint, deno fmt --check, and the doc tests.

Adds an opt-in useMaps boolean to the unstable parse() options: mappings
construct Map<unknown, unknown> at every depth, keys keep their parsed
types (3: is the number 3, "3": the string "3"), complex keys stay
structural, and duplicate keys compare SameValueZero. !!set constructs a
Set, !!omap a Map, and !!pairs an array of [key, value] pairs under the
flag. The unstable stringify() gains intrinsic Map/Set output (insertion
order, explicit ?-key form for collection keys, !!set tag for Sets) so
values round-trip; the stable modules are unchanged.

Internally the loader forks on a private container protocol
(create/has/set/entries) with plain-object and Map adapters, so the
legacy path keeps its direct-assignment fast path and __proto__
handling, and Maps are allocated where {} was allocated today so anchors
and cycles need no new mechanism.
@codecov

codecov Bot commented Aug 16, 2026

Copy link
Copy Markdown

Codecov Report

✅ All modified and coverable lines are covered by tests.
✅ Project coverage is 95.08%. Comparing base (ca58f94) to head (e126ab0).

Additional details and impacted files
@@            Coverage Diff             @@
##             main    #7290      +/-   ##
==========================================
+ Coverage   95.03%   95.08%   +0.04%     
==========================================
  Files         617      617              
  Lines       51637    51788     +151     
  Branches     9359     9414      +55     
==========================================
+ Hits        49075    49242     +167     
+ Misses       2021     2008      -13     
+ Partials      541      538       -3     

☔ View full report in Codecov by Harness.
📢 Have feedback on the report? Share it here.

🚀 New features to boost your workflow:
  • ❄️ Test Analytics: Detect flaky tests, report on failures, and find test suite problems.
  • 📦 JS Bundle Analysis: Save yourself from yourself by tracking and limiting bundle sizes in JS merges.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

Projects

None yet

Development

Successfully merging this pull request may close these issues.

yaml: add option for using Maps instead of objects for mappings

1 participant