"Respectful" YAML patching in Rust
May 8, 2026
Patching a YAML file programmatically is straightforward in principle: parse, modify, serialize. Ideally the process should also be respectful — that is, preserve the following properties of the initial file:
Formatting. The same YAML value can be represented in multiple ways: how mappings and lists are indented, whether blank lines separate sections, how strings are quoted, and so on. For example, a list can be represented in block style
items: - 1 - 2 - 3or in flow style
items: [1, 2, 3]A general-purpose YAML library typically picks one canonical form when serializing and applies it to the entire document.
Comments. One of YAML’s ergonomic advantages is that a value can have an associated inline note explaining why it’s set the way it is. Comments are typically erased at the deserialization stage and therefore have no chance to be serialized back.
Losing either property hurts. A dropped comment effectively loses historical context. Mangled formatting can render the resulting file invalid, or wipe out a layout that was carefully chosen for a specific situation (e.g. turn an intentional flow list into a block list).
Reaching for a popular general-purpose YAML library is the obvious move, but none of them preserve both:
serde_yamlis no longer maintained, and the feature request was declined as out of scope long before that.yaml-rust2doesn’t preserve comments; the feature request was closed with the note that the work would happen in saphyr instead.saphyris yaml-rust2’s spiritual successor by the same maintainer; comment support is planned but not yet shipped.
So a more niche tool is needed.
The candidates
A search of crates.io and lib.rs for libraries that claim comment preservation turns up four candidates:
yamlpath+yamlpatch— comment- and format-preserving routing (yamlpath) and patch operations (yamlpatch).yaml-edit— per its README, preserves formatting, comments, and whitespace.rust-yaml— README has a dedicated Comment Preservation section.yamp— README lists comment preservation as one of the project’s design goals.
The experiment
The example below uses a simplified config for a trading bot. The assets are
grouped into named groups with a catch-all default group:
# outer comment
asset_groups:
group_abc: # group_abc comment
- BTC
- ETH
- SOL
# group_xyz outer comment
group_xyz:
- DOGE # asset comment
- PEPE
default:
# default group inner comment
- 1INCH
- ATOM
- LINKThe toy CLI used here supports two operations:
list-assets ASSET1,ASSET2— append the listed assets to thedefaultgroup, in alphabetical order.delist-assets ASSET1,ASSET2— remove the listed assets from whichever group they live in. If a group goes empty, drop the group entirely.
Listing assets
The first test is a single list-assets invocation with four assets, picked to
exercise three cases at once:
list-assets 1INCH,BTC,XRP,BNB
1INCHis already indefault→ no-op.BTCis already ingroup_abc→ also no-op.XRPandBNBare new and should land indefault, alphabetically sorted alongside the existing items.
The expected output:
# outer comment
asset_groups:
group_abc: # group_abc comment
- BTC
- ETH
- SOL
# group_xyz outer comment
group_xyz:
- DOGE # asset comment
- PEPE
default:
# default group inner comment
- 1INCH
- ATOM
- BNB
- LINK
- XRP
yamlpath + yamlpatch — exact match
# outer comment
asset_groups:
group_abc: # group_abc comment
- BTC
- ETH
- SOL
# group_xyz outer comment
group_xyz:
- DOGE # asset comment
- PEPE
default:
# default group inner comment
- 1INCH
- ATOM
- BNB
- LINK
- XRP
yaml-edit —
outer comment dropped, "default" misindented
asset_groups:
group_abc: # group_abc comment
- BTC
- ETH
- SOL
# group_xyz outer comment
group_xyz:
- DOGE # asset comment
- PEPE
default:
# default group inner comment
- 1INCH
- ATOM
- BNB
- LINK
- XRP
rust-yaml — multiple issues, disqualified
# outer comment
asset_groups:
group_abc:
- BTC
- ETH
- SOL
group_xyz:
- DOGE
- PEPE
default:
- 1
- 1INCH
- ATOM
- BNB
- INCH
- LINK
- XRP
# group_abc comment
# outer comment
# outer comment
# group_abc comment
The comments are scattered (some end up at the bottom of the file, some duplicated),
1INCH is split into two list items (- 1 and - INCH), and the deliberate
whitespace and inline comment on DOGE are both lost.
The library’s own comment_preservation_demo.rs exhibits the
same comment-scattering behavior when run unmodified.
yamp — parsing issues, disqualified
No output is shown here because some of the comments in the input confuse yamp’s parser.
list-assets is the easier of the two operations since it only touches a single group
and only adds. yamlpath + yamlpatch round-trip the file exactly.
yaml-edit does violate both properties, but not severely enough to disqualify it on
this test alone.
Delisting assets
delist-assets is the more demanding operation: any group can be modified, any asset
can be removed, groups can be removed entirely. The test:
delist-assets DOGE,PEPE,BTC,SOL,ATOM,SHIB
That covers every interesting case at once:
DOGEandPEPEare both members ofgroup_xyz. Removing both should empty the group, which means the wholegroup_xyzgroup has to be removed.BTCandSOLcome out ofgroup_abc, leaving it with onlyETH.ATOMis removed fromdefault.SHIBisn’t in the file at all; should be a no-op.
The expected output:
# outer comment
asset_groups:
group_abc: # group_abc comment
- ETH
default:
# default group inner comment
- 1INCH
- LINK
yamlpath + yamlpatch —
almost, a single comment rearranged
# outer comment
asset_groups:
group_abc: # group_abc comment
- ETH # group_xyz outer comment
default:
# default group inner comment
- 1INCH
- LINK
When the now-empty group_xyz: key is removed, the standalone comment that was sitting
on the line above it doesn’t get removed with it. Instead it migrates onto the
nearest surviving content line as an inline comment. The output is valid YAML and
no comment is lost, but the comment is now attached to the wrong list item.
yaml-edit — logical structure changed, disqualified
asset_groups:
group_abc: # group_abc comment
- ETH
# group_xyz outer comment
default:
# default group inner comment
- 1INCH
- LINK
The indentation shift on default: is not just cosmetic: default is now nested
inside group_abc rather than being a sibling. The two top-level groups have
collapsed into one.
yamlpath + yamlpatch produces valid YAML, but the stranded comment is a
violation of the “respectfulness” properties — which is probably not a dealbreaker
given the state of other libraries.
The winner
yamlpath + yamlpatch is currently the best of the available options. It is not
perfect, but it is actively maintained and it can be made to work with some
workarounds and compromises. Here are some caveats I encountered while trying to make
it work for my actual use case.
Op::Replace doesn’t work on sequences
yamlpatch-replace-list.rs
| |
$ cargo run --example yamlpatch-replace-list -- --assets 1INCH,BTC,XRP,BNB
Error: apply patches
Caused by:
0: YAML query error: input is not valid YAML
1: input is not valid YAML
Updating a list requires a workaround
Since Op::Replace is unusable on sequences, updating a list end-to-end requires a
workaround: append the entire desired list to the end first, then remove the original
items from the front one at a time:
yamlpatch-rotate-replace-list.rs
| |
$ cargo run --example yamlpatch-rotate-replace-list -- --assets 1INCH,BTC,XRP,BNB
asset_groups:
default:
- 1INCH
- ATOM
- BNB
- BTC
- LINK
- XRP
It doesn’t play well with flow-style lists
yamlpatch-flow-list.rs
| |
$ cargo run --example yamlpatch-flow-list -- --assets 1INCH,BTC,XRP,BNB
Error: apply patches
Caused by:
Invalid operation: append operation is not permitted against flow sequence route: Route { route: [Key("asset_groups"), Key("default")] }
Conclusion
yamlpath + yamlpatch is the only option that comes truly close to “respectful”
patching as defined here. It is very much usable in practice, even though it doesn’t
cover every case out of the box.
Full accompanying source code can be found here. Built with rustc 1.95.0.
Library versions used: yamlpath 1.24.1, yamlpatch 1.24.1, yaml-edit 0.2.1,
rust-yaml 0.0.5, yamp 0.1.0.