7 unstable releases (3 breaking)
| new 0.4.0 | May 18, 2026 |
|---|---|
| 0.3.0 | May 13, 2026 |
| 0.2.1 | May 12, 2026 |
| 0.1.4 | May 12, 2026 |
#388 in Encoding
Used in structprop-validator
89KB
1.5K
SLoC
serde-structprop
A serde serializer and deserializer for the structprop configuration file format — a simple, human-readable format for structured data.
Format overview
Structprop files are composed of three constructs:
# Lines beginning with # are comments (inline comments are also supported)
# Scalar key-value pair
key = value
key = "value with spaces or special chars"
key = 42
key = -7
key = true
# Nested object block
section {
nested_key = value
another = 123
}
# Array of scalars
list = { a b c }
list = {
a
b
c
}
Special characters in values (spaces, tabs, newlines, carriage returns,
#, {, }, =) must be wrapped in double quotes. Empty strings are
always quoted as "". The structprop format has no escape sequences.
A " character in the interior of an otherwise-bare value is emitted
as-is (e.g. hello"world → val = hello"world). Two cases are
unrepresentable and produce a serialization error:
- A value that requires quoting and contains
"(no way to embed"inside a quoted term). - A value whose first character is
"(the lexer treats a leading"as the start of a quoted string).
Keys follow the same rule.
Installation
Add to your Cargo.toml:
[dependencies]
serde-structprop = { version = "0.2", features = ["derive"] }
The derive feature enables serde's own derive macros. You still need a
direct serde dependency in your crate so that Serialize and Deserialize
are in scope. If you already depend on serde with features = ["derive"],
you can omit the feature flag here:
[dependencies]
serde = { version = "1", features = ["derive"] }
serde-structprop = "0.2"
Type mapping
| Rust / serde type | Structprop representation |
|---|---|
bool |
true or false |
i8 i16 i32 i64 u8 u16 u32 u64 |
bare integer scalar (e.g. 42, -7) |
f32, f64 |
bare float scalar (e.g. 3.14) |
char |
bare single-character scalar |
String / &str |
bare scalar, or "quoted" when it contains special chars or is empty; a " mixed with other special chars is unrepresentable (serialization error) |
Option<T> (Some) |
the inner value serialized normally |
Option<T> (None) / () |
null |
newtype struct (e.g. struct Meters(f64)) |
transparent — serializes as the inner type |
unit struct (e.g. struct Marker;) |
null |
| struct / map | key { … } block |
Vec<T> / sequence |
key = { … } list |
| tuple / tuple struct | key = { … } list of elements |
| unit enum variant | bare variant name |
| newtype enum variant | variant_name = <scalar or list> |
| tuple enum variant | variant_name = { … } list |
| struct enum variant | variant_name { … } block |
raw bytes (serialize_bytes / deserialize_bytes) |
unsupported — returns Error::UnsupportedType |
Quick start
Deserializing
use serde::Deserialize;
use serde_structprop::from_str;
#[derive(Debug, Deserialize)]
struct Config {
hostname: String,
port: u16,
debug: bool,
}
fn main() {
let input = "
# server config
hostname = localhost
port = 8080
debug = true
";
let cfg: Config = from_str(input).unwrap();
println!("{cfg:?}");
// Config { hostname: "localhost", port: 8080, debug: true }
}
Serializing
use serde::Serialize;
use serde_structprop::to_string;
#[derive(Serialize)]
struct Config {
hostname: String,
port: u16,
debug: bool,
}
fn main() {
let cfg = Config {
hostname: "localhost".into(),
port: 8080,
debug: true,
};
let out = to_string(&cfg).unwrap();
println!("{out}");
// hostname = localhost
// port = 8080
// debug = true
}
Nested structs
use serde::{Deserialize, Serialize};
use serde_structprop::{from_str, to_string};
#[derive(Debug, Serialize, Deserialize, PartialEq)]
struct Database {
hostname: String,
port: u16,
name: String,
}
#[derive(Debug, Serialize, Deserialize, PartialEq)]
struct Config {
database: Database,
tables: Vec<String>,
}
fn main() {
let input = "
database {
hostname = db.example.com
port = 5432
name = myapp
}
tables = { users orders products }
";
let cfg: Config = from_str(input).unwrap();
assert_eq!(cfg.database.port, 5432);
assert_eq!(cfg.tables, vec!["users", "orders", "products"]);
// Round-trip back to structprop text
let out = to_string(&cfg).unwrap();
println!("{out}");
// database {
// hostname = db.example.com
// port = 5432
// name = myapp
// }
// tables = {
// users
// orders
// products
// }
}
Quoted values
Values containing spaces, tabs, newlines, or the special characters
(#, {, }, =) are quoted automatically on output and must be
quoted in the input. Empty strings are always quoted as "".
The structprop format has no escape sequences. A " in the interior
of an otherwise-bare value is emitted as-is (e.g. hello"world →
val = hello"world). Two cases are unrepresentable and return an error:
a value that requires quoting and contains ", and a value whose first
character is " (the lexer always interprets a leading " as the start
of a quoted string).
use serde::{Deserialize, Serialize};
use serde_structprop::{from_str, to_string};
#[derive(Debug, Serialize, Deserialize)]
struct S {
message: String,
empty: String,
}
fn main() {
let s: S = from_str(r#"message = "hello world"
empty = """#).unwrap();
assert_eq!(s.message, "hello world");
assert_eq!(s.empty, "");
let out = to_string(&s).unwrap();
assert_eq!(out, "message = \"hello world\"\nempty = \"\"\n");
}
Optional fields
Fields typed as Option<T> are serialized as null when None and as
the inner value when Some. A missing key in the input also deserializes
as None:
use serde::{Deserialize, Serialize};
use serde_structprop::{from_str, to_string};
#[derive(Debug, Serialize, Deserialize, PartialEq)]
struct S {
required: String,
optional: Option<u32>,
}
fn main() {
// Some value
let s: S = from_str("required = hello\noptional = 42\n").unwrap();
assert_eq!(s.optional, Some(42));
// Explicit null
let s: S = from_str("required = hello\noptional = null\n").unwrap();
assert_eq!(s.optional, None);
// Missing key also deserializes as None
let s: S = from_str("required = hello\n").unwrap();
assert_eq!(s.optional, None);
// None serializes as null
let out = to_string(&S { required: "hello".into(), optional: None }).unwrap();
assert!(out.contains("optional = null"));
}
Error handling
All functions return serde_structprop::Result<T>, an alias for
std::result::Result<T, serde_structprop::Error>.
use serde_structprop::{from_str, Error};
#[derive(serde::Deserialize)]
struct S { x: u32 }
match from_str::<S>("x = not_a_number\n") {
Ok(s) => println!("x = {}", s.x),
Err(Error::Parse(msg)) => eprintln!("parse error: {msg}"),
Err(Error::Message(msg)) => eprintln!("serde error: {msg}"),
Err(e) => eprintln!("other error: {e}"),
}
Error variants
| Variant | When |
|---|---|
Error::Parse(String) |
Lexer or parser encountered unexpected input, or a scalar could not be coerced to the requested numeric type |
Error::Message(String) |
serde-generated error (e.g. missing required field, unknown variant) |
Error::UnsupportedType(&'static str) |
Type has no structprop equivalent (e.g. raw byte slices) |
Error::KeyMustBeString |
A map was serialized with a non-string key |
Module layout
| Module | Contents |
|---|---|
serde_structprop::lexer |
Tokenizer: converts raw text to Tokens |
serde_structprop::parse |
Recursive-descent parser: produces a Value tree |
serde_structprop::de |
serde::Deserializer implementation; from_str entry point |
serde_structprop::ser |
serde::Serializer implementation; to_string entry point |
serde_structprop::error |
Error enum and Result<T> alias |
License
Licensed under either of
at your option.
Dependencies
~0.7–1.3MB
~22K SLoC