Skip to content

Comments and Round Trips

YAML files accumulate comments the way kitchens accumulate notes on the fridge: the reason a value is what it is, written beside the value, by someone who knew they'd forget. Most YAML tools throw those away without mentioning it — they parse to a data structure, and comments aren't data. This chapter is about the two ways comments fare in yq's world: editing with yq, which keeps them, and passing through JSON, which destroys them completely. The difference decides how you should route any automated edit.

Editing with yq: comments stay

The Compose file carries three comments — two at the top, one trailing the image line. Edit a different service's image, in place, and list what comments remain:

$ yq -i '.services.db.image = "postgres:16.4"' commented.yaml
$ grep -n '#' commented.yaml
1:# Fieldnotes stack — a small note-taking app.
2:# Start everything with: docker compose up
5:    image: fieldnotes/web:1.4.2 # bump the tag when a release lands

All three, unmoved. Chapter 4's diff showed the stronger version: the comment trailing the very line being edited survived its own edit. yq attaches comments to the nodes they annotate and carries them through the rewrite.

What it does not promise is your layout. Chapter 1's identity pass and chapter 4's diff both lost the blank line before volumes:. Think of it as: yq preserves everything that is part of the document — values, comments, key order — and re-typesets everything that is merely presentation. For config files, that trade is almost always worth making; for a file a human has carefully formatted as documentation, it might not be, and the diff habit will tell you in one look.

The JSON round trip: comments die

Now route the same file through JSON and back — the path data takes when a pipeline includes a tool that only speaks JSON (or the other yq, which is one):

$ yq -o=json '.' compose.yaml > compose.json
$ yq -p=json -o=yaml '.' compose.json > roundtrip.yaml
$ grep -c '#' roundtrip.yaml
0

Zero comments. (grep -c prints the count, 0, and itself exits 1 for "no matches" — the 1 is grep's, and zero is the finding.) JSON has no comment syntax, so there is nowhere for them to live during the crossing. The full diff shows they aren't the only casualties:

$ diff compose.yaml roundtrip.yaml
1,2d0
< # Fieldnotes stack — a small note-taking app.
< # Start everything with: docker compose up
5c3
<     image: fieldnotes/web:1.4.2 # bump the tag when a release lands
---
>     image: fieldnotes/web:1.4.2
7c5
<       - "8080:80"
---
>       - 8080:80
21d18
< 
23c20
<   db-data:
---
>   db-data: null

Read the losses in ascending order of subtlety. The comments are gone, all of them — expected, if you think about JSON for a moment. The blank line went too, but yq itself does that anyway. The port lost its quotes: "8080:80" came back as 8080:80. The value is the same string, and yq chose not to quote it on the way out — but a file's quoting style is sometimes a deliberate signal to the next reader, human or parser, and it's gone.

And the last hunk is the one that changes data: db-data: — a key with an empty value, which YAML reads as null — came back as db-data: null, spelled out. Same meaning to a YAML parser. Not the same file, and to a tool that distinguishes "present and empty" from "present and null" in its own way, not necessarily the same behaviour either.

The rule this teaches

Route Comments Key order Quoting style Empty values
Read and write with yq Kept Kept Mostly kept Kept
YAML → JSON → YAML Lost Kept Re-chosen Spelled out as null

So: do YAML edits in YAML, with a tool that models comments — which is to say, with this one, used the way chapters 4 and 10 use it. Convert to JSON when another tool genuinely needs JSON (chapter 6 does exactly that, into jq), but treat the conversion as one-way for editing purposes. A file that has been through JSON is a new file with the same data, and should be reviewed like one.