6 releases (breaking)
| 0.5.0 | Feb 2, 2026 |
|---|---|
| 0.4.0 | Jan 24, 2026 |
| 0.3.0 | Jan 22, 2026 |
| 0.2.0 | Jan 20, 2026 |
| 0.1.1 | Jan 19, 2026 |
#1133 in Parser implementations
Used in shyaml-rs
230KB
4K
SLoC
fyaml
A safe Rust wrapper around the libfyaml C library for parsing and
manipulating YAML documents.
Overview
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.
Status
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.
Why libfyaml?
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.
Features
- 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
ValueReftype: Zero-copy typed access with YAML type interpretationas_str(),as_bool(),as_i64(),as_f64(),is_null()- Non-plain scalars (quoted, literal, folded) preserved as strings
Valuetype: 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]
Error Handling
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.
Zero-Copy Architecture
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 time
This 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
Style and Comment Preservation
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.
What IS preserved:
- 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: |"));
Formatting notes:
- Flow collections (
[a, b],{a: 1}) are preserved as flow style but may be reformatted across multiple lines by libfyaml's emitter
Path Syntax
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");
Usage
Working with Value (high-level, owned)
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();
Zero-copy with Document and NodeRef
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");
Zero-copy typed access with ValueRef
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 -> true
Non-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));
Mutation with Editor
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");
Building structures programmatically with handle-level operations:
use fyaml::Document;
let mut doc = Document::new().unwrap();
{
let mut ed = doc.edit();
// Build a sequence of servers
let mut servers = ed.build_sequence().unwrap();
let s1 = ed.build_scalar("web1").unwrap();
let s2 = ed.build_scalar("web2").unwrap();
ed.seq_append(&mut servers, s1).unwrap();
ed.seq_append(&mut servers, s2).unwrap();
// Build the root mapping
let mut root = ed.build_mapping().unwrap();
let k1 = ed.build_scalar("host").unwrap();
let v1 = ed.build_scalar("localhost").unwrap();
ed.map_insert(&mut root, k1, v1).unwrap();
let k2 = ed.build_scalar("port").unwrap();
let v2 = ed.build_null().unwrap();
ed.map_insert(&mut root, k2, v2).unwrap();
let k3 = ed.build_scalar("servers").unwrap();
ed.map_insert(&mut root, k3, servers).unwrap();
// Tag the root
ed.set_tag(&mut root, "!config").unwrap();
ed.set_root(root).unwrap();
}
let root = doc.root().unwrap();
assert_eq!(root.at_path("/host").unwrap().scalar_str().unwrap(), "localhost");
assert_eq!(root.tag_str().unwrap().unwrap(), "!config");
assert_eq!(root.at_path("/servers/0").unwrap().scalar_str().unwrap(), "web1");
Multi-document parsing with FyParser
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");
Reading from stdin
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
}
Serde integration
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();
Iterating over mappings
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());
}
Iterating over sequences
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());
}
Checking node types
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());
API Reference
Main Types
| 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 |
Enums
| Type | Variants |
|---|---|
NodeType |
Scalar, Sequence,
Mapping |
NodeStyle |
Plain, SingleQuoted,
DoubleQuoted, Literal, Folded,
etc. |
Document Methods
| 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 |
NodeRef Methods (zero-copy)
| 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 |
ValueRef Methods (zero-copy typed access)
| 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) |
Editor Methods
| 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.build_null() |
Build null scalar node |
ed.set_root(handle) |
Set document root |
ed.copy_node(node) |
Copy node from any document |
ed.seq_append(seq, item) |
Append item to detached sequence handle |
ed.map_insert(map, key, val) |
Insert key-value pair into detached mapping |
ed.set_tag(node, tag) |
Set YAML tag on detached node |
ed.set_style(node, style) |
Set YAML style on detached node, returns actual style |
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 |
FyParser Methods
| 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>) |
Value Methods
| 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 |
Iterators
| 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 |
Dependencies
libc- C library bindingsfyaml-sys- FFI bindings to libfyamllog- Logging frameworkserde- Serialization frameworkindexmap- Order-preserving map for YAML mappings
Test Coverage
| Metric | Coverage |
|---|---|
| Lines | 89.05% |
| Functions | 91.83% |
| Regions | 91.22% |
Other Rust YAML Libraries
| 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 |
Choosing a Library
-
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.
License
MIT License (c) 2024-2026 Valentin Lab. The LICENSE file is available with the source.
Changelog
0.5.0 (2026-02-02)
New
-
Add
set_stylemethod toEditor[Valentin Lab]Allows setting the YAML style (plain, single-quoted, double-quoted, literal, folded, flow, block) on detached node handles. libfyaml validates the requested style against node content and may keep the current style if the request is invalid; the actually-applied style is returned.
-
Add handle-level
Editormethods for programmatic node assembly. [Valentin Lab]Add
seq_append(),map_insert(),set_tag(), andbuild_null()toEditorfor building YAML structures without parsing YAML snippets. These complement the existing path-based API (set_yaml_at,seq_append_at) with lower-level handle operations.Update
README.orgwith API table entries and a usage example.
Fix
-
Quote ambiguous strings during
ValueYAML emission. [Valentin Lab]Strings like "true", "null", "42" were emitted as plain scalars, causing them to be reinterpreted as bool/null/number on re-parse. Now
needs_quoting()detects such values and the emitter wraps them in single quotes so they roundtrip correctly asValue::String. -
build_null()now produces a proper YAML null node. [Valentin Lab]fy_node_create_scalar_copy(doc, NULL, 0)creates an empty scalar without libfyaml's internalis_nullflag, making it indistinguishable frombuild_scalar(""). Switch tobuild_from_yaml("null")which goes through the parser and setsis_null = true.Tighten null emission tests:
Value::Nullmust emit exactly"null", and must differ fromValue::String(""). -
Value::to_yaml_string()no longer adds trailing newline. [Valentin Lab]to_yaml_string()was using document-level emission (fy_emit_document_to_string) which appends\n. Rewrite to use node-level emission viaEditorAPI, matchingNodeRef::emit()behavior.Tighten existing test assertions that masked this with
.trim().
0.4.0 (2026-01-24)
New
-
Add sequence element support to
set_yaml_at[Valentin Lab]- Support positive and negative (Python-style) indices for sequences
- Validate index bounds and return error for out-of-bounds access
- Update documentation with sequence examples and supported parent types
- Add comprehensive tests for sequence manipulation edge cases
0.3.0 (2026-01-22)
New
-
Add rich parse errors with line/column location info. [Valentin Lab]
- Add
ParseErrortype withline(),column(),location()accessors - Add
diagmodule to capture libfyaml errors viafy_diagcallbacks - Enable
FYPCF_QUIETon all parse configs to suppress stderr output - Update
Document::parse_str,from_string,from_bytesto returnError::ParseErrorwith location info - Update
Editor::build_from_yamlwith RAIIDiagGuardfor diag restoration - Update
FyParserstream iterator to capture errors with location
This makes the library suitable for GUI applications and IDEs that need structured error information without stderr pollution.
- Add
-
Refactor to enforce zero-copy aspects wherever possible. [Valentin Lab]
0.2.0 (2026-01-20)
New
-
Add
from_stdin_with_line_buffer()for configurable stdin buffering. [Valentin Lab]Allows callers to control whether stdin uses line-buffered or block-buffered mode. Line-buffered is useful for streaming/interactive use (process documents as lines arrive), while block-buffered is more efficient for batch processing.
The existing
from_stdin()method now delegates to this with line_buffered=true to preserve backward compatibility.
Dependencies
~3–7MB
~167K SLoC