Facet
Reflection library for Rust. One #[derive(Facet)] gives you serialization, pretty-printing, diffing, and more — across any supported format.
#[derive(Facet)]
struct Config {
name: String,
port: u16,
#[facet(sensitive)]
api_key: String,
}Attribute Quick Reference
Container attributes (struct / enum)
| Attribute | Effect |
|---|---|
deny_unknown_fields | Error on unknown fields during deserialization |
default | Use Default for missing fields |
rename_all = "..." | Rename all fields/variants using a case convention |
transparent | Forward ser/de to inner type (newtype pattern) |
opaque | Hide inner structure; use with proxy for ser/de |
skip_all_unless_truthy | Apply skip_unless_truthy to every field |
type_tag = "..." | Add a type identifier for self-describing formats |
crate = path | Custom path to the facet crate (for re-exports) |
Enum representation attributes
| Attribute | Format | Example output |
|---|---|---|
untagged | No discriminator | 42 / "hello" |
tag = "type" | Internal tag as field | {"type":"Req","id":1} |
tag = "t", content = "c" | Adjacent tag+content | {"t":"Text","c":"hello"} |
Field attributes
| Attribute | Effect |
|---|---|
rename = "..." | Rename field in ser/de |
default / default = val / default = fn() | Default when missing |
skip | Skip in both ser and de (requires default) |
skip_serializing | Skip during serialization only |
skip_deserializing | Skip during deserialization (uses default) |
skip_serializing_if = pred | Conditionally skip serialization |
skip_unless_truthy | Skip when value is falsy (see truthiness rules) |
sensitive | Redact in debug/pretty output ([REDACTED]) |
flatten | Merge nested struct fields into parent |
child | Mark as child node (KDL, XML) |
invariants = fn | Validate after deserialization |
proxy = Type | Use proxy type for ser/de conversion |
opaque, proxy = Type | Opaque + proxy for non-Facet types |
Attribute Details
rename_all — Case conventions
"PascalCase" · "camelCase" · "snake_case" · "SCREAMING_SNAKE_CASE" · "kebab-case" · "SCREAMING-KEBAB-CASE"
transparent — Newtype pattern
#[derive(Facet)]
#[facet(transparent)]
struct UserId(u64); // Serialized as just the u64opaque — Hide inner structure
Fields don’t need to implement Facet. Cannot be serialized on its own — pair with proxy:
#[derive(Facet)]
struct Config {
name: String,
#[facet(opaque, proxy = SecretKeyProxy)]
key: SecretKey, // Serialized as hex string via proxy
}When
assert_same!encounters an opaque type, it returnsSameness::Opaque.
skip_unless_truthy — Truthiness rules
| Type | Truthy when |
|---|---|
bool | true |
| numbers | non-zero (floats exclude NaN) |
Vec, String, slices | non-empty |
Option | Some(_) |
| arrays | non-zero length |
default — Field-level options
#[facet(default)] // Uses Default::default()
#[facet(default = 8080)] // Literal value
#[facet(default = default_timeout())] // Function callinvariants — Post-deserialization validation
Called when partial.build() finalizes a value. Returns bool; false fails deserialization.
#[derive(Facet)]
#[facet(invariants = Range::is_valid)]
struct Range {
min: u32,
max: u32,
}
impl Range {
fn is_valid(&self) -> bool { self.min <= self.max }
}Limitation: Nested structs are not automatically validated — add an invariant to the parent to check nested values explicitly.
proxy — Custom ser/de representation
Required trait implementations:
TryFrom<ProxyType> for FieldType— deserialization (proxy → actual)TryFrom<&FieldType> for ProxyType— serialization (actual → proxy)
Pattern: Delegate to FromStr / Display
#[derive(Facet)]
#[facet(transparent)]
struct ColorProxy(String); // Must be transparent to serialize as plain string
impl TryFrom<ColorProxy> for Color {
type Error = String;
fn try_from(p: ColorProxy) -> Result<Self, Self::Error> { Color::from_str(&p.0) }
}
impl TryFrom<&Color> for ColorProxy {
type Error = std::convert::Infallible;
fn try_from(c: &Color) -> Result<Self, Self::Error> { Ok(ColorProxy(c.to_string())) }
}
#[derive(Facet)]
struct Theme {
#[facet(proxy = ColorProxy)]
foreground: Color,
}Pattern: Hex integers
#[derive(Facet)]
#[facet(transparent)]
struct HexU64(String);
// TryFrom<HexU64> for u64 — parses "0x..." hex string
// TryFrom<&u64> for HexU64 — formats as "0x{:x}"Errors & Diagnostics
Facet errors implement miette::Diagnostic — you get source spans, labels, and typo hints.
let user: User = facet_json::from_str(input).into_diagnostic()?;× unknown field `agge`, expected one of: ["name", "age"]
┌─ <stdin>:1:20
│
1 │ { "name": "Ada", "agge": 36 }
│ ──── did you mean `age`?
Tips:
- Add
miette = { version = "7", features = ["fancy"] }for color + unicode boxes - Pass a named reader for file spans
- Snapshot
miette::Report::new(err)in tests to lock error text
Dynamic Values (facet_value::Value)
Format-agnostic equivalent to serde_json::Value. Supports: Null, Bool, Number, String, Bytes, Array, Object, DateTime.
Two-phase deserialization
// 1. Deserialize to dynamic Value
let value: Value = facet_json::from_str(json)?;
// 2. Inspect or transform
println!("{:?}", value.as_object().map(|o| o.keys().collect::<Vec<_>>()));
// 3. Convert to concrete type
let config: Config = from_value(&value)?;Partial tree extraction
let value: Value = facet_json::from_str(json)?;
if let Some(db) = value.as_object().and_then(|o| o.get("database")) {
let db_config: DatabaseConfig = from_value(db)?;
}assert_same! — Cross-type structural comparison
No PartialEq required. Can compare a Value against a typed struct:
use facet_assert::assert_same;
let user = User { name: "Alice".into(), age: 30 };
let expected: Value = facet_json::from_str(r#"{"name":"Alice","age":30}"#)?;
assert_same!(user, expected); // Different types, same structurefacet-args — CLI
Turns any Facet struct into a CLI. Provides auto-generated help, shell completions, and rich error diagnostics.
Attributes
#[facet(args::positional)] // Positional argument
#[facet(args::named)] // Named flag (--field-name)
#[facet(args::named, args::short)] // Named + short flag (-f)
#[facet(args::short = 'o')] // Explicit short flag character
#[facet(args::subcommand)] // Subcommand dispatchParsing
use facet_args::from_std_args;
fn main() -> Result<(), Box<dyn std::error::Error>> {
let args: Args = from_std_args()?;
Ok(())
}Help generation
use facet_args::{generate_help, HelpConfig};
let config = HelpConfig {
program_name: Some("mytool".into()),
version: Some("1.0.0".into()),
..Default::default()
};
println!("{}", generate_help::<Args>(&config));Shell completions
use facet_args::{generate_completions, Shell};
let bash = generate_completions::<Args>(Shell::Bash, "mytool");
let zsh = generate_completions::<Args>(Shell::Zsh, "mytool");
let fish = generate_completions::<Args>(Shell::Fish, "mytool");Install locations:
- Bash:
~/.local/share/bash-completion/completions/mytool - Zsh:
~/.zsh/completions/_mytool - Fish:
~/.config/fish/completions/mytool.fish
Extension Attributes (Format Crates)
KDL (facet_kdl)
#[facet(kdl::node_name)] // Field holds the KDL node name
#[facet(kdl::argument)] // KDL positional argument
#[facet(kdl::property)] // KDL property
#[facet(kdl::children)] // Collection of child nodes (auto-singularizes field name)
#[facet(kdl::children = "tag")] // Children with explicit node nameEcosystem Integration
Third-party crate features
Enable in Cargo.toml:
[dependencies]
facet = { version = "...", features = ["uuid", "chrono"] }| Feature | Crate | Types |
|---|---|---|
uuid | uuid | Uuid |
ulid | ulid | Ulid |
url | url | Url |
chrono | chrono | DateTime<Tz>, NaiveDate, NaiveTime, NaiveDateTime |
time | time | Date, Time, PrimitiveDateTime, OffsetDateTime, Duration |
jiff02 | jiff | Timestamp, Zoned, DateTime, Date, Time, Span, SignedDuration |
camino | camino | Utf8Path, Utf8PathBuf |
bytes | bytes | Bytes, BytesMut |
ordered-float | ordered-float | OrderedFloat<f32/f64>, NotNan<f32/f64> |
ruint | ruint | Uint<BITS, LIMBS>, Bits<BITS, LIMBS> |
Standard library features
| Feature | Types |
|---|---|
nonzero | NonZero<T> variants (NonZeroU8, NonZeroI32, …) |
net | SocketAddr, IpAddr, Ipv4Addr, Ipv6Addr, SocketAddrV4, SocketAddrV6 |
tuples-12 | Tuples up to 12 elements (default: 4) |
fn-ptr | Function pointer types |
doc | Include /// doc comments in Shape/Field/Variant (increases binary size) |
Crate Index
| Crate | Description |
|---|---|
facet | Core derive macro + trait |
facet-json | JSON format |
facet-yaml | YAML format |
facet-toml | TOML format |
facet-kdl | KDL format |
facet-xml | XML format |
facet-csv | CSV format |
facet-msgpack | MessagePack format |
facet-urlencoded | URL-encoded format |
facet-args | CLI argument parsing |
facet-value | Dynamic Value type |
facet-pretty | Pretty-printing |
facet-diff | Structural diffing |
facet-validate | Validation |
facet-error | Error types |
facet-ansi | ANSI terminal output |
facet-svg | SVG output |
facet-html | HTML output |
facet-html-dom | HTML DOM |
facet-json-schema | JSON Schema generation |
facet-axum | Axum integration |
facet-tokio-postgres | tokio-postgres integration |
facet-singularize | Word singularization (used by KDL) |