A fully spec-compliant NestedText v3.8 parser and serializer for Rust.
nested-text passes 100% of the official NestedText test suite (KenKundert/nestedtext_tests):
- 146/146 load tests — including full validation of error messages, line numbers, column numbers, and source line text for all 68 error cases
- 80/80 roundtrip dump tests — every successfully loaded document survives a dump/load cycle with identical output
- 2 tests skipped (non-UTF-8 encoding tests — NestedText is a UTF-8 format)
The implementation mirrors the architecture of the Python reference implementation by Ken Kundert: a line-based lexer feeding a recursive descent parser.
Add to your Cargo.toml:
[dependencies]
nested-text = "0.1"serde support is enabled by default. To disable it:
[dependencies]
nested-text = { version = "0.1", default-features = false }use nested_text::{loads, dumps, Top, Value, DumpOptions};
// Parse a NestedText string
let input = "name: Alice\nage: 30\nitems:\n - one\n - two\n";
let value = loads(input, Top::Any).unwrap().unwrap();
// Access values
assert_eq!(value.get("name").unwrap().as_str(), Some("Alice"));
assert_eq!(value.get("age").unwrap().as_str(), Some("30"));
// Serialize back to NestedText
let output = dumps(&value, &DumpOptions::default());
let roundtripped = loads(&output, Top::Any).unwrap().unwrap();
assert_eq!(value, roundtripped);use nested_text::{loads, Top};
// Require a specific top-level type
let dict = loads("key: value", Top::Dict).unwrap();
let list = loads("- item", Top::List).unwrap();
let string = loads("> hello", Top::String).unwrap();
// Empty documents return a default value for the given type
let empty_dict = loads("", Top::Dict).unwrap(); // Some(Dict([]))
let empty_any = loads("", Top::Any).unwrap(); // Noneuse serde::{Deserialize, Serialize};
#[derive(Deserialize, Serialize)]
struct Config {
name: String,
debug: bool,
port: u16,
tags: Vec<String>,
}
let input = "\
name: my-app
debug: true
port: 8080
tags:
- web
- api
";
// Deserialize — numeric and boolean fields are parsed from strings
let config: Config = nested_text::from_str(input).unwrap();
assert_eq!(config.name, "my-app");
assert_eq!(config.debug, true);
assert_eq!(config.port, 8080);
// Serialize back to NestedText
let output = nested_text::to_string(&config).unwrap();use nested_text::{load, Top};
use std::fs::File;
let file = File::open("config.nt").unwrap();
let value = load(file, Top::Any).unwrap();Errors include location information matching the reference implementation:
use nested_text::{loads, Top};
match loads("key: a\nkey: b", Top::Any) {
Err(e) => {
assert_eq!(e.message, "duplicate key: key.");
assert_eq!(e.lineno, Some(1)); // 0-based
assert_eq!(e.colno, Some(0)); // 0-based
assert_eq!(e.line.as_deref(), Some("key: b"));
}
_ => panic!("expected error"),
}use nested_text::{Value, dumps, DumpOptions};
let value = Value::Dict(vec![
("name".into(), Value::String("Alice".into())),
("scores".into(), Value::List(vec![
Value::String("95".into()),
Value::String("87".into()),
])),
]);
let nt = dumps(&value, &DumpOptions::default());
// name: Alice
// scores:
// - 95
// - 87| Function | Description |
|---|---|
loads(input, top) -> Result<Option<Value>> |
Parse a NestedText string |
load(reader, top) -> Result<Option<Value>> |
Parse from any Read implementor |
from_str<T>(input) -> Result<T> |
Deserialize via serde (requires serde feature) |
| Function | Description |
|---|---|
dumps(value, options) -> String |
Serialize a Value to NestedText |
dump(value, options, writer) -> Result<()> |
Serialize to any Write implementor |
to_string(value) -> Result<String> |
Serialize via serde (requires serde feature) |
to_string_with_options(value, options) -> Result<String> |
Serialize via serde with custom options |
Value—String(String),List(Vec<Value>),Dict(Vec<(String, Value)>)- Dicts preserve insertion order using
Vec<(String, Value)> - Convenience methods:
as_str(),as_list(),as_dict(),get(key),is_string(),is_list(),is_dict()
- Dicts preserve insertion order using
Top—Dict,List,String,AnyDumpOptions—indent: usize(default 4),sort_keys: bool(default false)Error—message,lineno(0-based),colno(0-based),line,kind
NestedText is a data format similar to YAML but with no type ambiguity — all leaf values are strings. No quoting, no escaping, no type coercion surprises.
# This is a comment
server:
host: localhost
port: 8080
features:
- logging
- metrics
database:
> postgres://localhost
> :5432/mydb
For the full specification, see nestedtext.org.
Clone with the official test suite submodule and run:
git clone --recurse-submodules https://github.com/hansstimer/nested-text.git
cd nested-text
cargo testTo see detailed pass/fail counts:
cargo test --test official -- --nocaptureThis runs the full official NestedText test suite automatically:
- Load tests — decodes each test case from
tests.json, parses the input, and compares the result againstload_out. For error cases, validates the error message, line number, column number, and source line text againstload_err. - Roundtrip dump tests — for every successful load test, serializes the parsed value back to NestedText, parses it again, and verifies the result is identical.
If you already cloned without --recurse-submodules:
git submodule update --initLicensed under either of
- Apache License, Version 2.0 (LICENSE-APACHE or http://www.apache.org/licenses/LICENSE-2.0)
- MIT license (LICENSE-MIT or http://opensource.org/licenses/MIT)
at your option.