A safe Rust wrapper around the libfyaml C library for parsing and
manipulating YAML documents.
fyaml provides an idiomatic Rust interface to the high-performance
libfyaml YAML parsing library. It supports DOM-style navigation,
zero-copy scalar access, node type introspection, multi-document
parsing, document mutation, and a serde-compatible Value type.
Early development - This library is functional but has not yet been
widely used or audited. The API may change, and edge cases may exist.
If you need a mature, battle-tested YAML library, consider serde_yml
or serde-yaml-ng instead.
fyaml is built on libfyaml, a modern C library that offers
several advantages over the traditional libyaml:
- Full YAML 1.2 compliance with YAML 1.3 preparation
- Zero-copy architecture for efficient large document handling
- No artificial limits (libyaml has a 1024-char implicit key limit)
- Up to 24x faster on large files in streaming mode (vs document mode)
- Rich manipulation APIs including YPATH expressions for path queries
- MIT licensed (as of v0.9.1)
This makes fyaml suitable for use cases requiring DOM manipulation,
YAML transformation tools, or configuration inspection utilities where
path-based queries are convenient.
- Parse YAML strings into document objects
- Zero-copy scalar access via lifetime-bound
NodeRefandValueRef - Navigate nodes using path-based queries (e.g.,
/foo/bar,/list/0) - Support for all YAML node types: scalars, sequences, and mappings
- Iterate over mapping key-value pairs and sequence items
- Convert nodes back to YAML strings
- Multi-document stream parsing via
FyParser - Read YAML from stdin (single document or stream)
- Document mutation via
Editorwith compile-time safety - Style and comment preservation during edits: comments, quote styles, block/flow structure
- ~ValueRef~ type: Zero-copy typed access with YAML type interpretation
as_str(),as_bool(),as_i64(),as_f64(),is_null()- Non-plain scalars (quoted, literal, folded) preserved as strings
- ~Value~ type: Pure Rust enum with serde support
- Serialize/deserialize with any serde-compatible format (JSON, TOML, etc.)
- Emit YAML using libfyaml for standards-compliant output
- Convenient indexing:
value["key"],value[0]
Parse errors include detailed location information (line and column numbers), making it easy to report errors to users or integrate with IDEs and linters.
use fyaml::Document;
let result = Document::parse_str("[unclosed bracket");
if let Err(e) = result {
// Access structured error info
if let fyaml::Error::ParseError(parse_err) = &e {
println!("Error: {}", parse_err.message());
if let Some((line, col)) = parse_err.location() {
println!("At line {}, column {}", line, col);
}
}
// Or just display it nicely
println!("{}", e); // "Parse error at 2:1: flow sequence without a closing bracket"
}The ParseError type provides:
message()- The error message from libfyamlline()- Line number (1-based), if availablecolumn()- Column number (1-based), if availablelocation()- Tuple of (line, column) if both available
All parsing methods (Document::parse_str, Document::from_string,
Document::from_bytes, Editor::build_from_yaml) capture errors silently
without printing to stderr, making the library suitable for use in GUI
applications and test suites.
fyaml leverages libfyaml’s zero-copy design for efficient memory usage.
When you access scalar values through NodeRef or ValueRef, you get
references directly into the parsed document buffer - no string copying
or allocation occurs.
use fyaml::Document;
let doc = Document::parse_str("message: Hello, World!").unwrap();
let root = doc.root().unwrap();
let node = root.at_path("/message").unwrap();
// Zero-copy: this &str points directly into the document's memory
let s: &str = node.scalar_str().unwrap();
assert_eq!(s, "Hello, World!");
// The reference is tied to the document's lifetime -
// this prevents use-after-free at compile timeThis is particularly beneficial for:
- Large documents: Read gigabytes of YAML without doubling memory usage
- Config parsing: Extract only the values you need without copying everything
- High-throughput processing: Minimize allocations in hot paths
When modifying documents with Editor, fyaml preserves formatting and
comments. This is essential for configuration files where maintaining the
original style improves readability and diff-friendliness.
- Comments: Top-level, inline, and end-of-line comments
- Quote styles: Single-quoted (
'value'), double-quoted ("value"), and plain scalars - Block scalar styles: Literal (
|) and folded (>) blocks - Collection styles: Flow (
[a, b],{a: 1}) vs block (indented) sequences/mappings
use fyaml::Document;
// Comments and quote styles are preserved through edits
let yaml = "# Database configuration
database:
host: 'localhost' # local dev server
port: 5432
";
let mut doc = Document::parse_str(yaml).unwrap();
{
let mut ed = doc.edit();
ed.set_yaml_at("/database/port", "5433").unwrap();
}
let output = doc.emit().unwrap();
// Comments preserved
assert!(output.contains("# Database configuration"));
assert!(output.contains("# local dev server"));
// Quote style preserved
assert!(output.contains("'localhost'"));Block scalars are also preserved:
use fyaml::Document;
let yaml = "script: |
echo hello
echo world
name: test
";
let mut doc = Document::parse_str(yaml).unwrap();
{
let mut ed = doc.edit();
ed.set_yaml_at("/name", "modified").unwrap();
}
let output = doc.emit().unwrap();
// Literal block style (|) is preserved for the script
assert!(output.contains("script: |"));- Flow collections (
[a, b],{a: 1}) are preserved as flow style but may be reformatted across multiple lines by libfyaml’s emitter
Paths use / as the separator (YPATH/JSON Pointer style):
/key- access a mapping key/0- access a sequence index/parent/child/0- nested access
use fyaml::Document;
let yaml = "
database:
host: localhost
ports:
- 5432
- 5433
";
let doc = Document::parse_str(yaml).unwrap();
let root = doc.root().unwrap();
// Access mapping key
let db = root.at_path("/database").unwrap();
assert!(db.is_mapping());
// Nested access
let host = root.at_path("/database/host").unwrap();
assert_eq!(host.scalar_str().unwrap(), "localhost");
// Sequence index
let first_port = root.at_path("/database/ports/0").unwrap();
assert_eq!(first_port.scalar_str().unwrap(), "5432");The Value type provides a convenient, serde-compatible way to work with YAML:
use fyaml::Value;
// Parse YAML
let value: Value = "name: Alice\nage: 30".parse().unwrap();
// Access values with indexing
assert_eq!(value["name"].as_str(), Some("Alice"));
assert_eq!(value["age"].as_i64(), Some(30));
// Emit back to YAML
let yaml = value.to_yaml_string().unwrap();For more control and zero-copy scalar access, use the Document API:
use fyaml::Document;
let doc = Document::parse_str("database:\n host: localhost\n port: 5432").unwrap();
let root = doc.root().unwrap();
// Zero-copy: returns &str pointing into document memory
let host = root.at_path("/database/host").unwrap();
assert_eq!(host.scalar_str().unwrap(), "localhost");
// Navigation by path
let port = root.at_path("/database/port").unwrap();
assert_eq!(port.scalar_str().unwrap(), "5432");ValueRef wraps NodeRef and provides typed accessors that interpret
YAML scalars on demand without allocation:
use fyaml::Document;
let doc = Document::parse_str("name: Alice\nage: 30\nactive: yes").unwrap();
let root = doc.root_value().unwrap();
// Zero-copy typed access
assert_eq!(root.get("name").unwrap().as_str(), Some("Alice"));
assert_eq!(root.get("age").unwrap().as_i64(), Some(30));
assert_eq!(root.get("active").unwrap().as_bool(), Some(true)); // yes -> trueNon-plain scalars (quoted, literal |, folded >) are NOT type-interpreted:
use fyaml::Document;
let doc = Document::parse_str("quoted: 'true'\nunquoted: true").unwrap();
let root = doc.root_value().unwrap();
// Quoted: string, not bool
assert_eq!(root.get("quoted").unwrap().as_bool(), None);
assert_eq!(root.get("quoted").unwrap().as_str(), Some("true"));
// Unquoted: interpreted as bool
assert_eq!(root.get("unquoted").unwrap().as_bool(), Some(true));Use Document::edit() to get an exclusive Editor for modifications:
use fyaml::Document;
let mut doc = Document::parse_str("name: Alice").unwrap();
// Mutation phase - NodeRef cannot exist during this
{
let mut ed = doc.edit();
ed.set_yaml_at("/name", "'Bob'").unwrap(); // Preserve quotes
ed.set_yaml_at("/age", "30").unwrap(); // Add new key
ed.delete_at("/name").unwrap(); // Delete key
ed.set_yaml_at("/name", "\"Charlie\"").unwrap(); // Re-add
}
// Read phase - safe to access nodes again
let root = doc.root().unwrap();
assert_eq!(root.at_path("/name").unwrap().scalar_str().unwrap(), "Charlie");
assert_eq!(root.at_path("/age").unwrap().scalar_str().unwrap(), "30");Building complex structures:
use fyaml::Document;
let mut doc = Document::new().unwrap();
{
let mut ed = doc.edit();
let root = ed.build_from_yaml("users:\n - name: Alice\n - name: Bob").unwrap();
ed.set_root(root).unwrap();
}
assert!(doc.root().is_some());Modifying sequence elements:
use fyaml::Document;
let mut doc = Document::parse_str("items:\n - a\n - b\n - c").unwrap();
{
let mut ed = doc.edit();
// Replace by positive index
ed.set_yaml_at("/items/0", "first").unwrap();
// Replace by negative index (Python-style: -1 = last element)
ed.set_yaml_at("/items/-1", "last").unwrap();
}
assert_eq!(doc.at_path("/items/0").unwrap().scalar_str().unwrap(), "first");
assert_eq!(doc.at_path("/items/1").unwrap().scalar_str().unwrap(), "b");
assert_eq!(doc.at_path("/items/2").unwrap().scalar_str().unwrap(), "last");Use FyParser for parsing YAML streams with multiple documents:
use fyaml::FyParser;
let yaml = "---\ndoc1: value1\n---\ndoc2: value2";
let parser = FyParser::from_string(yaml).unwrap();
let docs: Vec<_> = parser.doc_iter().filter_map(|r| r.ok()).collect();
assert_eq!(docs.len(), 2);
// Each document is independent
assert_eq!(docs[0].at_path("/doc1").unwrap().scalar_str().unwrap(), "value1");
assert_eq!(docs[1].at_path("/doc2").unwrap().scalar_str().unwrap(), "value2");For CLI tools that read YAML from stdin:
use fyaml::Document;
// Single document from stdin
let doc = Document::from_stdin().unwrap();
println!("{}", doc.emit().unwrap());For multi-document streams:
use fyaml::FyParser;
// Default: line-buffered mode for interactive/streaming use
let parser = FyParser::from_stdin().unwrap();
for doc_result in parser.doc_iter() {
let doc = doc_result.unwrap();
println!("{}", doc.emit().unwrap());
}For batch processing where efficiency matters more than interactivity:
use fyaml::FyParser;
// Block-buffered mode: more efficient for large inputs
let parser = FyParser::from_stdin_with_line_buffer(false).unwrap();
for doc_result in parser.doc_iter() {
// Process each document
}Value works with any serde-compatible format:
use fyaml::Value;
let value: Value = "key: value".parse().unwrap();
// Convert to JSON
let json = serde_json::to_string(&value).unwrap();
assert_eq!(json, r#"{"key":"value"}"#);
// Parse from JSON
let from_json: Value = serde_json::from_str(&json).unwrap();use fyaml::Document;
let doc = Document::parse_str("a: 1\nb: 2\nc: 3").unwrap();
let root = doc.root().unwrap();
for (key, value) in root.map_iter() {
println!("{}: {}", key.scalar_str().unwrap(), value.scalar_str().unwrap());
}use fyaml::Document;
let doc = Document::parse_str("- apple\n- banana\n- cherry").unwrap();
let root = doc.root().unwrap();
for item in root.seq_iter() {
println!("{}", item.scalar_str().unwrap());
}use fyaml::{Document, NodeType};
let doc = Document::parse_str("key: value").unwrap();
let root = doc.root().unwrap();
assert!(root.is_mapping());
assert_eq!(root.kind(), NodeType::Mapping);
let value = root.at_path("/key").unwrap();
assert!(value.is_scalar());| Type | Description |
|---|---|
Document | Parsed YAML document (owns the data) |
NodeRef<'doc> | Zero-copy reference to a node (borrows document) |
ValueRef<'doc> | Zero-copy typed access (wraps NodeRef) |
Editor<'doc> | Exclusive mutable access to document |
FyParser | Multi-document stream parser |
Value | Owned serde-compatible YAML value |
Number | Numeric value: Int(i64), UInt(u64), Float(f64) |
TaggedValue | Value with an associated YAML tag |
ParseError | Rich parse error with line/column location |
| Type | Variants |
|---|---|
NodeType | Scalar, Sequence, Mapping |
NodeStyle | Plain, SingleQuoted, DoubleQuoted, Literal, Folded, etc. |
| Method | Description |
|---|---|
Document::parse_str(yaml) | Parse YAML string into Document |
Document::new() | Create empty document |
Document::from_stdin() | Parse single document from stdin |
doc.root() | Get root node as Option<NodeRef> |
doc.root_value() | Get root node as Option<ValueRef> |
doc.at_path(path) | Navigate to node by path |
doc.edit() | Get exclusive Editor for mutations |
doc.emit() | Emit document as YAML string |
| Method | Description |
|---|---|
node.kind() | Get node type (NodeType) |
node.style() | Get node style (NodeStyle) |
node.is_scalar() | Check if node is a scalar |
node.is_mapping() | Check if node is a mapping |
node.is_sequence() | Check if node is a sequence |
node.is_quoted() | Check if scalar is quoted |
node.is_non_plain() | Check if scalar has non-plain style |
node.scalar_str() | Get scalar as &str (zero-copy) |
node.scalar_bytes() | Get scalar as &[u8] (zero-copy) |
node.at_path(path) | Navigate to child by path |
node.seq_iter() | Iterate over sequence items |
node.map_iter() | Iterate over mapping key-value pairs |
node.seq_len() | Get sequence length |
node.map_len() | Get mapping length |
node.seq_get(i) | Get sequence item by index |
node.map_get(key) | Get mapping value by string key |
node.tag_str() | Get YAML tag (zero-copy) |
node.emit() | Emit node as YAML string |
| Method | Description |
|---|---|
value.as_str() | Get string (zero-copy) Option<&str> |
value.as_bool() | Interpret as boolean (yes/no/on/off/true/false) |
value.as_i64() | Interpret as signed integer (hex/octal/binary) |
value.as_u64() | Interpret as unsigned integer |
value.as_f64() | Interpret as float (.inf, .nan support) |
value.is_null() | Check for null/~/empty |
value.is_scalar() | Check if scalar |
value.is_sequence() | Check if sequence |
value.is_mapping() | Check if mapping |
value.get(key) | Get mapping value by key |
value.index(i) | Get sequence item by index |
value.at_path(path) | Navigate by path |
value.seq_iter() | Iterate over sequence as ValueRef |
value.map_iter() | Iterate over mapping as (ValueRef, ValueRef) |
value.tag() | Get YAML tag (zero-copy) |
| Method | Description |
|---|---|
ed.set_yaml_at(path, yaml) | Set/replace value at path (mappings and sequences) |
ed.delete_at(path) | Delete value at path, returns bool |
ed.build_from_yaml(yaml) | Build detached node from YAML |
ed.build_scalar(value) | Build plain scalar node |
ed.build_sequence() | Build empty sequence node |
ed.build_mapping() | Build empty mapping node |
ed.set_root(handle) | Set document root |
ed.copy_node(node) | Copy node from any document |
ed.seq_append_at(path, item) | Append item to sequence at path |
ed.root() | Read root during edit session |
ed.at_path(path) | Navigate during edit session |
| Method | Description |
|---|---|
FyParser::from_string(yaml) | Create parser from YAML string |
FyParser::from_stdin() | Create parser from stdin (line-buffered) |
FyParser::from_stdin_with_line_buffer(b) | Configurable buffering |
parser.doc_iter() | Iterate over documents (yields Result<Document>) |
| Method | Description |
|---|---|
parse() | Parse YAML string into Value |
to_yaml_string() | Emit as YAML string via libfyaml |
is_null() | Check if value is null |
is_bool() | Check if value is boolean |
is_number() | Check if value is numeric |
is_string() | Check if value is a string |
is_sequence() | Check if value is a sequence |
is_mapping() | Check if value is a mapping |
is_tagged() | Check if value has a tag |
as_str() | Get as &str if string |
as_i64() | Get as i64 if numeric |
as_u64() | Get as u64 if numeric |
as_f64() | Get as f64 if numeric |
as_bool() | Get as bool if boolean |
as_sequence() | Get as &[Value] if sequence |
as_mapping() | Get as &IndexMap if mapping |
as_tagged() | Get as &TaggedValue if tagged |
get(key) | Get value by key from mapping |
[key] / [idx] | Index into mapping or sequence |
| Type | Yields | Description |
|---|---|---|
SeqIter<'doc> | NodeRef<'doc> | Sequence items |
MapIter<'doc> | (NodeRef<'doc>, NodeRef<'doc>) | Mapping key-value pairs |
DocumentIterator | Result<Document> | Documents in stream |
libc- C library bindingsfyaml-sys- FFI bindings to libfyamllog- Logging frameworkserde- Serialization frameworkindexmap- Order-preserving map for YAML mappings
| Metric | Coverage |
|---|---|
| Lines | 88.44% |
| Functions | 91.29% |
| Regions | 90.44% |
| Library | Engine | Serde | Status |
|---|---|---|---|
| serde_yaml | unsafe-libyaml (libyaml transpiled to Rust) | Yes | Deprecated (2024-03) |
| serde_yml | unsafe-libyaml | Yes | Maintained (fork of serde_yaml) |
| serde-yaml-ng | unsafe-libyaml | Yes | Active (migrating to libyaml-safer) |
| saphyr | Pure Rust (fork of yaml-rust) | Soon | Active |
| yaml-rust2 | Pure Rust (fork of yaml-rust) | No | Active (high MSRV) |
| yaml-rust | Pure Rust | No | Unmaintained |
| fyaml | libfyaml (C library via FFI) | Yes | Development |
- For serde integration:
fyamlprovides a serde-compatibleValuetype with libfyaml-powered parsing and emission. Alternatives includeserde_ymlorserde-yaml-ng(based on unsafe-libyaml). - For pure Rust: Use
saphyroryaml-rust2(no C dependencies, easier to audit). - For DOM manipulation and path queries:
fyamlprovides convenient path-based navigation (/foo/0/bar) via libfyaml’s YPATH support, plus aValuetype for programmatic manipulation. - For maximum performance on large files:
fyamlbenefits from libfyaml’s zero-copy architecture and streaming optimizations.
MIT License (c) 2024-2026 Valentin Lab. The LICENSE file is available with the source.