Skip to main content
Back to BlogCode Guides

How to Format YAML (and Why the Comments Always Disappear)

Formatting YAML is not the same as formatting JSON or XML, because YAML has comments, and almost every formatter drops them. The mechanism is the problem: most YAML formatters work by parsing the document into an in-memory tree and re-dumping it from the tree. The tree has no comments, because the parser threw them away during the parse. When the dumper writes the tree back out, there are no comments left to write. This is true of js-yaml, the parser behind most browser-based YAML formatters, and it is true of PyYAML in Python. The comments are gone the moment you parse, and no amount of dump options will bring them back. Learn how a YAML formatter actually works (parse the document with js-yaml into a JavaScript object, then dump the object back to a YAML string with your chosen indent, quote style, line width, and flow level), why comments are always lost (the parser discards them during the parse; the dumper has nothing to re-emit), what your options are when you need to keep comments (use the eemeli/yaml library, which preserves comments in its AST, or use prettier with the yaml plugin, which does the same — or edit by hand), how the indent, quote-style, line-width, and flow-level options affect the output (indent sets the spaces per level, quote style forces single or double quotes on all strings, line width wraps long scalars, flow level switches from block style to flow style with braces and brackets), why multi-document YAML files (with --- separators) need special handling (each document is parsed and dumped separately, then rejoined), how anchors and aliases work (an anchor &name marks a value, an alias *name references it, a merge key <<: *name pulls in the anchored mapping — js-yaml resolves these during parse, and the dump option noRefs controls whether they reappear as anchors or get inlined), why the 'Tab indentation' option in some formatters is a lie (js-yaml does not support tab characters for indentation in its dump; the button sets the indent to 2 spaces regardless), and when to use a formatter at all (when you inherited an inconsistently indented file, when you want to normalize quote style, when you want to convert between YAML and JSON).

The Toolbox TeamAugust 13, 20268 min read

The problem: formatting YAML is not like formatting JSON or XML

YAML is the configuration format that took over after JSON proved too rigid and XML proved too verbose. Kubernetes configs, CI pipelines, GitHub Actions, Ansible playbooks, Docker Compose files — all YAML. The format is line-sensitive, indentation-sensitive, and supports comments with the # prefix. Those three properties are what make a YAML formatter harder to build than a JSON formatter, and what make most YAML formatters quietly wrong about the one thing that matters.

The mechanism is the problem. A YAML formatter does its job by parsing the document into an in-memory tree and re-dumping the tree as a new YAML string. The parse step converts the text into a structured object — keys, values, nested mappings, sequences. The dump step converts the object back into text with whatever formatting options you chose. This works cleanly for JSON, because JSON has no comments. It works cleanly for XML, because XML parsers preserve comments in their DOM tree. It fails for YAML, because the most common YAML parser — js-yaml in JavaScript, PyYAML in Python — discards comments during the parse. The tree has no comments. The dump has no comments. Your # this is the production database annotation, which was the only thing telling the next operator which value to change, is gone.

The YAML Formatter runs on js-yaml and has this exact limit. It also has a useful set of options — indent, quote style, line width, flow level, sort keys, expand aliases — that work correctly. The skill of formatting YAML is knowing which options to use and when to accept the comment loss versus when to walk away and use a different tool.

Fastest path

Open the YAML Formatter. Paste your YAML, drag a file in, upload one, or fetch from a URL. Pick an indent (2 or 4), a quote style (auto, single, double), a line width (80, 120, or infinite), and a flow level (block, flow at top, mixed). Toggle sort keys, expand aliases, and the lint rules. The tool validates as you type, shows formatted YAML, JSON, a minified flow-style version, a tree view, and a diff against your input. Copy or download the result. The whole thing runs in your browser.

How a YAML formatter actually works

The formatter does two things: parse and dump. The parse step calls yaml.load(input), which returns a JavaScript object representing the document. For multi-document files (separated by ---), it calls yaml.loadAll(input), which returns an array of objects. The dump step calls yaml.dump(object, options), which serializes the object back to a YAML string with the formatting you asked for.

The options are the only real controls you have:

  • indent sets the number of spaces per nesting level. 2 is the YAML community default; 4 is common in teams that came from JSON.
  • lineWidth sets the column at which long scalars wrap. 80 is the traditional width, 120 fits modern screens, and infinite disables wrapping entirely.
  • flowLevel switches between block style (the readable, indentation-based format) and flow style (the JSON-like format with braces and brackets). -1 is full block, 0 is flow at the top level, 1 is flow below depth 1. Most YAML you read is block style; flow style is for compact output.
  • quotingType and forceQuotes control whether strings are quoted and with what. Auto lets the dumper decide based on the string content (strings that look like numbers or booleans get quoted, plain strings do not). Single or double with forceQuotes forces every string to be quoted.
  • sortKeys sorts the keys of each mapping alphabetically. Useful for normalizing configs where the key order is not meaningful; harmful when the key order carries meaning (ordered stages in a pipeline).
  • noRefs controls anchors and aliases. When true, the dumper inlines the values of any anchors instead of emitting &name and *name. When false, the dumper re-emits the anchors if it detects shared object references.

The parse-then-dump design is why the comments are lost. The dumper is writing from the object, and the object has no comments.

The comment problem, and what to do about it

Comments are the single biggest reason people reach for a YAML formatter and then walk away disappointed. A Kubernetes config with # bump this on every deploy annotations, a CI pipeline with # this stage only runs on main explanations, a Docker Compose file with # do not change in production warnings — all of these are load-bearing comments that the team relies on. A formatter that drops them is not safe to run on those files.

There are three honest options when you need to keep comments.

Use a comment-preserving formatter. The eemeli/yaml library (note: not js-yaml — a different library) preserves comments in its AST and re-emits them on dump. Prettier with the prettier-plugin-yaml or the built-in YAML support uses a comment-preserving parser and keeps comments. These tools are the right answer when the comments matter. The browser-based formatter on this site does not use them, because they are larger and slower than js-yaml.

Format by hand. For a small file, manual formatting is faster than a tool. Fix the indentation, normalize the quotes, sort the keys if you want, and leave the comments where they are. This is the only option that gives you full control over the output, and for a 50-line config it takes two minutes.

Accept the loss and re-add the comments. For a large file where the comments are sparse and you can re-add them from memory, run the formatter, then manually re-insert the comments. This works once. It does not work as a recurring workflow, because the comments drift out of sync with reality the second time you forget to re-add one.

The choice depends on the file. For a generated config with no comments, the formatter is safe. For a hand-maintained config with comments, use a comment-preserving tool or edit by hand.

Multi-document YAML, and why it needs special handling

A YAML file can contain multiple documents separated by --- on its own line. Kubernetes uses this for multi-resource files; CI pipelines sometimes use it for matrix configs. Each document is an independent YAML stream and parses to its own object.

The formatter handles this by parsing each document separately and rejoining the dumps with --- separators. The output is a valid multi-document YAML file with the same document count as the input. The JSON conversion wraps multiple documents in an array; a single document is output as a plain object.

The thing to watch is that the per-document formatting is independent. If one document has a comment and another does not, the comment loss applies to the first and the second is unaffected. If you are converting a multi-document file to JSON, you get an array of objects, and the order is preserved.

Anchors, aliases, and merge keys

YAML has three features for reusing content within a document: anchors (&name), aliases (*name), and merge keys (<<: *name). An anchor marks a value, an alias references it, and a merge key pulls the anchored mapping's keys into the current mapping. They are the YAML equivalent of variables and includes.

The formatter handles these through js-yaml. During parse, js-yaml resolves anchors and aliases into shared JavaScript object references — the same object in memory is pointed to by the anchor and the alias. During dump, the noRefs option controls whether the dumper re-emits the anchors or inlines the values. With noRefs: true (the "expand aliases" toggle), the dumper inlines the values and drops the anchor/alias syntax. With noRefs unset, the dumper re-emits the anchors if it detects shared references in the object graph.

The practical effect: if your input uses anchors to avoid repetition, the formatted output will either preserve them (expand aliases off) or inline them (expand aliases on). Inlining makes the file larger but easier to read for someone who does not know the anchor names. Preserving keeps the file smaller but requires the reader to follow the references. Most teams prefer to preserve, because the anchors are usually there for a reason.

Merge keys (<<: *name) are resolved during parse — the anchored mapping's keys are pulled into the current mapping. After parse, there is no merge key in the object; there are just the merged keys. The dump will not re-emit the merge key unless the formatter is specifically built to detect and re-create it. js-yaml does not re-create merge keys. If your input uses <<: *name to share defaults across several mappings, the formatted output will have the defaults inlined into each mapping, and the merge key syntax will be gone. This is a one-way transformation; you cannot get the merge key back from the formatted output.

The indent, quote, and flow options, and what each does

Indent sets the spaces per level. 2 is the YAML community default and what most linters expect. 4 is fine if your team prefers it, but it is not the convention. Some formatters offer a "tab" option that does not actually work — js-yaml does not support tab characters for indentation in its dump, and the button silently falls back to 2 spaces. If you need tab indentation, you need a different library.

Quote style controls how strings are quoted. Auto (the default) lets the dumper decide: strings that would be misparsed (like true, 123, or strings with leading spaces) get quoted, plain strings do not. Forcing single or double quotes wraps every string, which is verbose but unambiguous. For configs that will be read by humans, auto is usually right. For configs that will be machine-processed and need to round-trip cleanly, forcing quotes can prevent ambiguity.

Line width sets the wrap column for long scalars. 80 is the traditional width. 120 fits modern editors. Infinite disables wrapping entirely, which is the right choice for YAML that contains long strings (like embedded certificates or base64 blobs) that should never wrap. A wrapped base64 blob is a corrupted base64 blob when someone copies it back together.

Flow level switches between block and flow style. Block is the readable, indentation-based format most YAML uses. Flow is the JSON-like format with braces and brackets. Flow at the top level (flowLevel: 0) produces a single-line mapping with {key: value, key: value} syntax. This is what the "minified" output tab does. Flow is more compact but harder to read, and it loses the visual structure that makes YAML useful. Use flow for short configs where the compactness matters, block for everything else.

When to use a formatter at all

A YAML formatter is the right tool when you inherited a file with inconsistent indentation, when you want to normalize quote style across a team, when you want to convert between YAML and JSON, and when you want to validate a file you are not sure is well-formed. It is the wrong tool when the file has comments you need to keep, when the key order carries meaning, when the file uses merge keys you want to preserve, and when you need tab indentation. For those cases, use a comment-preserving formatter (eemeli/yaml, prettier with the yaml plugin) or edit by hand.

Gotchas

  • Comments are always lost. The formatter parses with js-yaml and re-dumps, and js-yaml discards comments during the parse. The dumper has no comments to re-emit. For files with load-bearing comments, use a comment-preserving tool (eemeli/yaml, prettier with the yaml plugin) or edit by hand.
  • Tab indentation is a lie. The "Tab" button sets a flag, but js-yaml does not support tab characters for indentation in its dump. The output uses 2 spaces regardless. If you need tab indentation, you need a different library.
  • Merge keys are not preserved. A <<: *name merge key is resolved during parse — the anchored mapping's keys are pulled into the current mapping. The dump will not re-emit the merge key syntax. The formatted output has the merged keys inlined, and you cannot get the merge key back.
  • Minified output is flow style, not true minification. The "minified" tab uses flow level 0, which produces {key: value} syntax with braces. For deeply nested structures, flow style can be longer than block style. The "X percent smaller" badge is computed from the actual lengths, and can be negative if flow style is longer.
  • Forcing quotes wraps every string. The single and double quote options set forceQuotes: true, which wraps every string including ones that do not need it. This is verbose. Use auto unless you have a specific reason to force.
  • sortKeys changes meaning. Sorting keys alphabetically is safe for a config where key order is decorative. It is harmful for a pipeline where key order is the order of execution. Check before you toggle it.
  • The lint rules are heuristics. The duplicate-key check tracks keys by indentation level and clears deeper levels when a shallower key appears. This works for simple block-style mappings but can miss duplicates in flow-style mappings or nested sequences. Treat the linter as advisory, not authoritative.
  • URL fetch goes through a third-party proxy. Fetching a YAML file by URL routes through a public CORS proxy. The proxy may be rate-limited, may log the URLs you fetch, and may be down. For sensitive configs, paste or upload instead.
  • The whole file loads into memory. There is no file size limit in the code. The entire input is parsed at once, and the formatted output is accumulated in a string. A 100 MB YAML file can freeze the tab. For large files, use a command-line formatter.

Summary

  • Formatting YAML is harder than formatting JSON or XML because YAML has comments, and the most common parser (js-yaml) discards them during the parse. The formatter parses the document into an object and re-dumps it, and the object has no comments. The dump has no comments. This is true of js-yaml, PyYAML, and most browser-based formatters.
  • The formatting options are indent (2 or 4 spaces), line width (80, 120, or infinite), flow level (block, flow at top, mixed), quote style (auto, single, double), sort keys, and expand aliases. Each does what it says. The "Tab" option is a lie — js-yaml does not support tab indentation in its dump, and the output uses 2 spaces regardless.
  • Multi-document YAML files (separated by ---) are handled by parsing each document separately and rejoining the dumps. The JSON conversion wraps multiple documents in an array. Anchors and aliases are resolved during parse and re-emitted during dump (unless expand aliases is on, which inlines them). Merge keys (<<: *name) are resolved during parse and not re-emitted — the merged keys appear inlined in the output.
  • Use the YAML Formatter for files without load-bearing comments, for normalizing indent and quote style, and for converting between YAML and JSON. Use a comment-preserving tool (eemeli/yaml, prettier with the yaml plugin) for files with comments you need to keep. Use the YAML to JSON Converter for the one-way conversion, the JSON Formatter for JSON input, and the XML Formatter for the XML equivalent.