Why YAML formatters delete your comments
Paste a commented Kubernetes manifest into most online YAML formatters and the comments come back gone. This is not a bug in those tools. It is a direct consequence of which function they call — and the same library will either keep every comment or destroy every comment depending on that one choice. Here are measured numbers for five libraries, and the code to reproduce them.
Results
Each library parsed the same 24-line Kubernetes Deployment containing 6 comments— two file-header lines, three standalone explanatory lines and three trailing inline notes — then serialised it straight back out. No transformation, no key changes. A perfect round-trip returns 6 of 6.
| Library | API used | Comments kept |
|---|---|---|
js-yaml 4.1.0JavaScript | load() + dump() | 0 / 6 |
yaml 2.8.1JavaScript | parse() + stringify() | 0 / 6 |
yaml 2.8.1JavaScript | parseDocument() + toString() | 6 / 6 |
PyYAML 6.0.3Python | safe_load() + dump() | 0 / 6 |
ruamel.yamlPython | YAML() round-trip mode | 6 / 6 |
The interesting row is the middle one. yaml version 2 appears twice, with opposite outcomes. Same library, same version, same input — parse() loses every comment and parseDocument() keeps every comment.
Why this happens
A YAML comment is not data. It is not a key, not a value, and it has nowhere to live inside a plain JavaScript object or a Python dict. So the moment a parser converts your document into native language types, the comments have already been thrown away — there was no field to put them in. Serialising that object back out cannot recover information that no longer exists.
This is why js-yaml, PyYAML, and yaml's parse() all score zero. They are not being careless; they are doing exactly what a load-into-native-types parser does. The comments are gone before dump() is ever called.
The two libraries that score 6 of 6 work differently. parseDocument()and ruamel.yaml's round-trip mode build a syntax tree rather than a plain object — a structure that models the document as written, with comments attached to the nodes they sit beside. Serialising that tree walks the original structure and re-emits the comments in place.
The cost is that you are no longer holding a plain object. Reading a value means asking the document for it rather than using ordinary property access, and that friction is exactly why so many tools reach for the simpler API and inherit the comment loss along with it.
A second finding: PyYAML reorders your keys
Unrelated to comments but worth knowing, because it surprises people in the same way. PyYAML's dump() defaults to sort_keys=True, which alphabetises every mapping:
>>> import yaml
>>> yaml.dump(yaml.safe_load("zebra: 1\napple: 2\nmango: 3\n"))
'apple: 2\nmango: 3\nzebra: 1\n'On a Kubernetes manifest this moves apiVersion and kind away from the top and scatters related settings apart. Pass sort_keys=False to stop it.js-yaml preserves insertion order by default and needs no equivalent flag.
Reproduce it
Two files. Install js-yaml@4 and yaml@2 from npm, plusruamel.yaml from pip, then run each script against this input.
input.yaml
# Deployment for the checkout service.
# Owned by the payments team — ask before changing replicas.
apiVersion: apps/v1
kind: Deployment
metadata:
name: checkout
labels:
app: checkout # used by the Service selector
spec:
# 3 is the minimum for the rolling update to stay available.
replicas: 3
template:
spec:
containers:
- name: checkout
image: registry.example.com/checkout:3.14.2
resources:
limits:
# Raised from 512Mi after the Nov 3 OOMKill incident.
memory: 1Gi
env:
- name: TIMEOUT_MS
value: "2500" # keep in sync with the gatewayJavaScript
import fs from 'fs';
import jsyaml from 'js-yaml';
import * as YAML from 'yaml';
const src = fs.readFileSync('input.yaml', 'utf8');
const count = (s) => (s.match(/#/g) || []).length;
console.log('source ', count(src));
console.log('js-yaml load+dump ', count(jsyaml.dump(jsyaml.load(src))));
console.log('yaml parse+stringify', count(YAML.stringify(YAML.parse(src))));
console.log('yaml parseDocument ', count(YAML.parseDocument(src).toString()));Python
import io, yaml as pyyaml
from ruamel.yaml import YAML
src = open('input.yaml').read()
print('source ', src.count('#'))
print('PyYAML load+dump ', pyyaml.dump(pyyaml.safe_load(src), sort_keys=False).count('#'))
y = YAML()
buf = io.StringIO()
y.dump(y.load(src), buf)
print('ruamel round-trip ', buf.getvalue().count('#'))Counting # characters is a deliberately blunt measure. It is sufficient here because the test document contains no # inside any string value, so every occurrence is a real comment. On documents where that is not true, count comment nodes instead of characters.
What to do about it
- In your own JavaScript code: use
yamlv2'sparseDocument()whenever the document will be written back out. Useparse()only when you are reading values and discarding the document. - In your own Python code: use
ruamel.yamlin round-trip mode for anything you re-serialise. Keep PyYAML for read-only loading, and passsort_keys=Falseif you ever dump with it. - Picking an online tool: paste a document with a comment in it before you trust the tool with a real manifest. It takes five seconds and tells you which of the two groups above the tool belongs to.
Our own YAML formatter uses parseDocument(), which is why it scores 6 of 6 on the test above. It also handles multi-document streams without duplicating the --- separators, and it runs entirely in your browser, so the manifest you paste is never transmitted anywhere. You can verify that claim by opening your browser's Network tab and watching no request get made.
Versions tested
Measured 19 August 2026 on Node.js 22.22.2 and Python 3.13: js-yaml 4.1.0,yaml 2.8.1, PyYAML 6.0.3, ruamel.yaml latest release. If you re-run this on newer versions and get different numbers, we would like to know — the results above are a snapshot, not a permanent property of these libraries.
