Recursive AI

Getting started with oneyaml for Rust

All oneyaml Rust docs

Add the oneyaml crate, read YAML into Rust types with serde, write it back, edit a file in place without losing its comments, and apply JSON Patch and merge patches.

oneyaml is serde and lossless, comment-preserving editing for YAML 1.2, in one crate. Both run on one parser, so a typed read and an edit never disagree about what a file means. Every sample on this page was compiled and run against oneyaml 1.2.0 from crates.io; the output under each one is what it printed.

Install

oneyaml needs Rust 1.85 or newer (edition 2024).

Terminal
cargo new yaml-start
cd yaml-start
cargo add [email protected] serde -F serde/derive

The only runtime dependency by default is serde. The crate has no unsafe code (#![forbid(unsafe_code)]), and every fallible call returns a Result or an Option instead of panicking. The license is MIT or Apache-2.0, at your choice.

The samples are complete main.rs files. A few read a file from the current directory; the YAML they read is shown with them.

Read YAML into a struct

Derive Deserialize and call oneyaml::from_str. Fields bind by name, the way they do with serde_json.

RUST
use serde::Deserialize;
 
#[derive(Debug, Deserialize)]
struct Config {
    name: String,
    replicas: u32,
    ports: Vec<u16>,
    debug: bool,
}
 
fn main() -> Result<(), oneyaml::Error> {
    let src = "\
name: api
replicas: 2
ports: [80, 443]
debug: false
";
    let cfg: Config = oneyaml::from_str(src)?;
    println!(
        "{} runs {} replicas on {:?} (debug: {})",
        cfg.name, cfg.replicas, cfg.ports, cfg.debug
    );
    Ok(())
}
Text
api runs 2 replicas on [80, 443] (debug: false)

YAML 1.2 is the default: yes and no are strings, not booleans, and 0755 is the decimal 755. A field of the wrong type is an error with a code, a line and a column, and the path to the value (see errors with line and column).

Write YAML

oneyaml::to_string writes any Serialize value as block-style YAML.

RUST
use serde::Serialize;
use std::collections::BTreeMap;
 
#[derive(Serialize)]
struct Config {
    name: String,
    replicas: u32,
    ports: Vec<u16>,
    labels: BTreeMap<String, String>,
}
 
fn main() -> Result<(), oneyaml::Error> {
    let cfg = Config {
        name: "api".into(),
        replicas: 3,
        ports: vec![80, 443],
        labels: BTreeMap::from([("tier".into(), "web".into())]),
    };
    print!("{}", oneyaml::to_string(&cfg)?);
    Ok(())
}
Text
name: api
replicas: 3
ports:
  - 80
  - 443
labels:
  tier: web

to_writer writes to any io::Write, and to_string_with takes Settings for the indent and other output choices.

Edit a file and keep its comments

To change a file that people also edit by hand, parse it into a Document instead. An untouched document writes back byte for byte; an edit changes only what it touches, and every comment stays with the entry it belongs to.

RUST
use oneyaml::Document;
 
fn main() -> Result<(), Box<dyn std::error::Error>> {
    let src = "\
# Deploy settings
name: api
replicas: 2   # raised by the release job
ports: [80]
";
    let mut doc = Document::parse(src)?;
 
    // Read a value.
    let replicas = doc.get_path("replicas").and_then(|n| n.as_u64());
    println!("replicas = {replicas:?}");
 
    // Change a value, append to a list, add a key.
    doc.set_path("replicas", 3)?;
    doc.get_mut("ports")?.push(443)?;
    doc.root_mut()?.insert("region", "us-east-1")?;
 
    print!("{doc}");
    Ok(())
}
Text
replicas = Some(2)
# Deploy settings
name: api
replicas: 3   # raised by the release job
ports: [80, 443]
region: us-east-1

Paths are strings such as "server.port" or "spec.containers[0].image". set_path keeps the old value’s style: a quoted string stays quoted, a block scalar stays a block scalar. New content is laid out like its neighbors, so the flow list [80] stays a flow list. The rules for which entry a comment belongs to are in comment attachment.

Apply a JSON Patch or a merge patch

A Document applies JSON Patch (RFC 6902) and merge patches (RFC 7396). Both are all or nothing: if any operation fails, the document is exactly as it was. Like the setters, a patch changes only the text of the values it names.

RUST
use oneyaml::{Document, MergePatch, Patch};
 
fn main() -> Result<(), Box<dyn std::error::Error>> {
    let src = "\
# Web service
image: web:1.0   # pinned
env:
  DEBUG: \"1\"
  LOG: info
ports: [80]
";
    // JSON Patch (RFC 6902): all or nothing.
    let mut doc = Document::parse(src)?;
    let patch = Patch::from_json(
        r#"[{"op": "replace", "path": "/image", "value": "web:1.1"},
            {"op": "add", "path": "/ports/-", "value": 443}]"#,
    )?;
    doc.apply_patch(&patch)?;
    print!("{doc}");
    println!("---");
 
    // Merge patch (RFC 7396): null removes a key.
    let mut doc = Document::parse(src)?;
    let merge = MergePatch::from_json(r#"{"env": {"DEBUG": null, "LOG": "warn"}}"#)?;
    doc.apply_merge_patch(&merge)?;
    print!("{doc}");
    Ok(())
}
Text
# Web service
image: web:1.1   # pinned
env:
  DEBUG: "1"
  LOG: info
ports: [80, 443]
---
# Web service
image: web:1.0   # pinned
env:
  LOG: warn
ports: [80]

Patches work on the YAML data model: test compares YAML values, so 1 is not 1.0, and writes below an alias are refused. A move carries the value without its comments; to move an entry with them, use NodeMut::move_entry. Every difference from JSON is listed in patches and pointers.

Where next

Recipes, each a complete program:

The docs:

Choosing a Rust YAML crate after serde_yaml: Rust YAML parser.