Diffing YAML Files Without Whitespace Noise or Reordering
diff -w hides YAML indentation changes that alter meaning. Normalize both files with yq sort_keys and compare as data to ignore layout and order only.

A pull request changes a YAML file and the diff is a wall of red and green: someone reformatted the file, reordered the keys and switched a list to flow style, and the real change, one value, is somewhere in the middle. The usual answer is to ignore whitespace, and in YAML that answer is dangerous. This post shows why diff -w is the wrong tool, and builds the right one from yq: normalize both files to a canonical form and compare that, so layout and key order are ignored and nothing else is.
Why whitespace cannot be ignored
In YAML, whitespace at the start of a line is structure. These two files differ only in leading whitespace on the last line:
a:
b: 1
c: 2a:
b: 1
c: 2In the first, c is a key inside a. In the second, c is a top-level key. They are different configurations. Run the whitespace-ignoring diff:
diff -w ind1.yaml ind2.yaml; echo "exit=$?"exit=0Nothing printed, exit status 0: "no differences". The flag treats lines that differ only in whitespace as equal, which is exactly the kind of change that moves a setting from one block to another. The plain diff shows it:
3c3
< c: 2
---
> c: 2So: never review YAML with a whitespace-insensitive diff. GitHub's "hide whitespace changes" option on a pull request does the same thing; leave it off for YAML.
Normalize, then diff
The right idea is to remove differences that do not matter, key order, flow versus block style, spacing, quote style, by converting both files to a canonical text form and diffing that. yq can do it in one expression: -P pretty-prints in block style with consistent indentation, and sort_keys(..) sorts the keys of every mapping recursively:
diff <(yq -P 'sort_keys(..)' old.yaml) <(yq -P 'sort_keys(..)' new.yaml) && echo "semantically identical"With these files:
name: app
replicas: 2
env:
B: "2"
A: "1"
image: web:1.0image: web:1.0
env: {A: "1", B: "2"}
name: app
replicas: 2semantically identicalThe plain diff old.yaml new.yaml printed a screen of changes (reordered keys, flow style, extra spaces); the normalized diff found none. This is the diff you want for a "pure refactor" pull request: it proves nothing changed in meaning. And for the structural-change pair above, the normalized diff still reports the nesting change (line 3, c: 2 indented or not), because indentation survives normalization as structure.
What sort_keys does and does not sort
printf 'z: [3, 1, 2]\na: {y: 1, x: 2}\n' | yq -P 'sort_keys(..)' -a:
x: 2
y: 1
z:
- 3
- 1
- 2Mapping keys are sorted at every level (a before z, x before y). List order is left alone, which is correct, since list order is meaningful: [3, 1, 2] and [1, 2, 3] are different lists. Do not sort lists in a normalizer unless you know order is irrelevant for those lists, for example a set of tags. If it is, a targeted (.tags) |= sort handles it for those paths only.
Comments
The normalized text includes comments, so a comment change is a difference:
1c1
< # c1
---
> # c2 changedWhether you want that is a decision. For a review of meaning, strip comments before comparing, which yq does with ... comments="":
diff <(yq -P 'sort_keys(..) | ... comments=""' a.yaml) <(yq -P 'sort_keys(..) | ... comments=""' b.yaml) && echo "identical without comments"identical without commentsKeep two commands, one that includes comments (a full review) and one that excludes them (a semantic check), and use the right one for the question.
Comparing as data
A normalized text diff still shows changes as lines of text. A comparison of the parsed data can report changes by path, which reads better for large nested files. In Python:
a = yaml.safe_load(open("old.yaml")); b = yaml.safe_load(open("new.yaml"))
print("equal as data:", a == b)
def diff(x, y, p=""):
if isinstance(x, dict) and isinstance(y, dict):
for k in sorted(set(x) | set(y), key=str):
if k not in x: print(f"+ {p}/{k}: {y[k]!r}")
elif k not in y: print(f"- {p}/{k}: {x[k]!r}")
else: diff(x[k], y[k], f"{p}/{k}")
elif x != y:
print(f"~ {p}: {x!r} -> {y!r}")On two small files:
equal as data: True
~ /a: 1 -> 2
- /b/d: 3
- /e: [1]
+ /f: 'x'Each line is a change at a path: ~ changed, - removed, + added. It ignores comments, order and style entirely, because the parser discarded them. It also applies the parser's type rules: NO and False compare equal after PyYAML loads them, so a change from one to the other is invisible. Use the same parser the application uses, and remember that this compares meaning as that parser sees it.
Wiring it into review
A refactor check in CI. For a pull request labeled "formatting only", run the normalized diff between the base branch version and the head version of each changed YAML file, and fail if it finds a difference. It turns a reviewer's promise into a test.
A Git diff driver. Configure
git diffto normalize YAML withtextconv, as in the XML equivalent (pretty printing XML for cleaner Git history):*.yaml diff=yamlin.gitattributesandgit config diff.yaml.textconv "yq -P 'sort_keys(..)'". Local diffs then ignore key order and style. Do this only if the order of keys is not meaningful for your files.Review with both. The normalized diff to see what changed in meaning, and the raw diff to see what changed in layout, because a layout change can also hide a mistake.
A worked review
Suppose a pull request modifies a 400-line deployment file and the raw diff shows 380 changed lines. The author says "just formatting". How do you check? Run the normalized comparison between the base version and the head version: git show main:deploy.yaml > /tmp/base.yaml, then diff the two normalized outputs. If it prints nothing, the claim is true, and you can approve the pull request after a glance at the raw diff for anything odd, such as quotes that were removed. If it prints five lines, those five lines are the real change, and you review only them. In both cases the review took a minute instead of an hour, and it did not depend on trusting the author or on reading 380 lines.
The reverse case matters too. When the normalized diff is empty and the author expected a change, either the edit did not apply or the change was only in a comment. And when the raw diff is tiny but the normalized diff is large, the file has been reordered by the normalizer's rules and the original order carried meaning; stop and check whether sorting is safe for this file.
A last practical point: normalize in CI the same way you normalize locally. If the pipeline uses a different yq version than your laptop, the canonical text may differ in small ways, and the check will report false differences. Pin the version, and run both sides of any comparison through the same binary.
Related reading
The cluster hub is working with YAML from the terminal. The risks of reformatting are in fixing YAML indentation errors with a linter, and the data comparison in Python in reading and writing YAML in Python safely. diff options are in the GNU diffutils manual, and sort_keys in the yq sort keys documentation. Compare two snippets in the text diff tool.
When a normalized diff hides what you need to see
Key order is meaningful. Some tools read keys in order, such as pipelines defined as mappings. Do not sort those.
Comments carry information. Use the version that keeps them.
Quote style matters to the consumer. Normalization may remove quotes that a YAML 1.1 reader needs. Compare with the raw diff as well.
Anchors are involved. Normalization expands or moves anchors in some tools. Check the result for files that use them.


