Facet
9 min read

Reflection framework for Rust. One #[derive(Facet)] describes a type once, and every capability — serialization, pretty-printing, diffing, CLI parsing, schema generation, GUI inspection — is a separate consumer reading that description.

rust
#[derive(Facet)]
struct Config {
name: String,
port: u16,
#[facet(sensitive)]
api_key: String,
}

Core model

The distinction that explains the whole ecosystem: serde generates code, facet generates data.

A serde derive emits functions. Attributes are consumed at compile time and burned into a control flow graph, so by runtime the type description no longer exists. A facet derive emits a const-evaluated value — a Shape describing the type's kind, layout, fields, documentation, attributes and type-specific operations — which survives into the running program.

Three consequences follow:

• Attributes survive. Error messages can name fields, suggest corrections and carry byte spans, because the metadata is still there when the error is built.
• Shapes are comparable at runtime. The same derive can treat Vec<u8> as bytes and Vec<u32> as integers by comparing shapes, a decision code generation has to fix at compile time.
• Consumers can arrive later. A crate written after your type still works on it, with no recompilation of your code and no derive to add.

The cost is honest and stated in the upstream docs: runtime traversal replaces monomorphised code, so facet trades some throughput for expressiveness, smaller binaries and one derive instead of several.

CrateRole
facetUmbrella crate: #[derive(Facet)], the Facet trait, public entry points
facet-coreDefines Shape, the Def tree, type metadata and pointer vocabulary
facet-reflectReads and builds values of arbitrary shapes via Peek and Partial
facet-macrosThe derive macro itself, built on unsynn



Ecosystem

Roughly forty crates read the same Shape. They are grouped below by capability rather than by name, which makes the pattern visible: each group is further from "serialise a struct" than the last, and none of them required the others to exist.

Data formats

CrateFormat
facet-jsonJSON, with span-aware diagnostics
facet-tomlTOML
facet-yamlYAML
facet-msgpackMessagePack
facet-postcardPostcard, compact binary
facet-csvCSV rows and records
facet-asn1ASN.1 DER/BER
facet-xdrXDR binary
facet-urlencodedapplication/x-www-form-urlencoded form data

Document / XML family

CrateDescription
facet-xmlXML serialization and deserialization
facet-domTree-based DOM layer shared by HTML and XML support
facet-svgStrongly typed SVG documents on top of facet-xml
facet-atomAtom Syndication Format (RFC 4287) documents

Schema & code generation

The shape turns out to be language-neutral; only the emitter is Rust-specific.

CrateEmits
facet-json-schemaJSON Schema documents
facet-typescriptTypeScript type definitions
facet-zodZod schemas (runtime validation + inferred TS types)
facet-pythonPython type definitions

Diagnostics & derive plugins

Not serialization at all — these are traversals of the same tree with a different visitor.

CrateDescription
facet-prettyPretty-prints with structure, color and sensitive-field redaction
rediffStructural diffing with path-aware reports; no PartialEq required
facet-assertassert_same! — cross-type structural comparison in tests
facet-defaultDerives Default with per-field custom default values
facet-errorDerives Error implementations from enum variants and doc comments
facet-validateRuns validation attributes during deserialization

Configuration & CLI

CrateDescription
figueLayers CLI args, environment variables, config files and defaults into one typed model
facet-styxSerializes and deserializes Styx documents
styx-cliStyx validation, formatting, schema generation and language-server tooling
facet-cargo-tomlParses Cargo.toml manifests and Cargo.lock into typed models

figue is the clearest small demonstration of the model: four unrelated input sources are unified because the shape is the join key, and none of them has to agree with any other.

Web, RPC & UI

CrateDescription
facet-axumFacet-backed extractors and responses for axum
voxRust-native RPC with cross-language codegen and multiple transports
facet-eguiAn egui inspector and editor widget for any Facet type

facet-egui is the sharpest illustration: nobody writes a UI for your struct, the UI is a function of the shape, and it works on types written before the crate existed.

Database

CrateDescription
rusqlite-facetBinds SQLite query parameters and maps rows through reflected structs

Runtime & incremental computation

CrateDescription
picanteTokio-first incremental query runtime: memoization, automatic dependency tracking, early cutoff, snapshot isolation
fableTiny typed language evaluated over reflected values; resolves field paths against shapes at compile time, then runs bytecode against live references without serialization
weavyLowered-program IR shared by interpreters and copy-and-patch JIT backends

Building blocks

CrateDescription
facet-valueDynamic values: JSON-like data plus bytes
facet-solverResolves type shapes from field names and constraints
facet-pathTracks paths through nested structures
stridStrongly typed string identifiers with Facet integration

Spun-out projects

Grown out of the ecosystem into independent repositories, each with its own documentation:

ProjectDescription
styxConfiguration language with native parsers for Rust, Python, JavaScript/TS and Go
dibsDatabase workflow: tables as Rust structs, generated migrations, queries, deploys
vixDemand-graph language — programs describe rather than run; evaluation happens only where demand reaches
rodinSolver specification with conflict learning, used by vix

vix is where the model is taken furthest: there are no statements and no control flow, a build and a type-check and a test are all just demands on one graph, and identity is split into three planes — location (where in the graph), recipe (operation plus input hashes) and content (the hash of the result).

Renamed or no longer listed

Previously documented here, absent from the current ecosystem listing:

OldStatus
facet-argsCLI handling now covered by figue
facet-diffNow rediff
facet-html, facet-html-domFolded into facet-dom
facet-kdlNo longer listed (attributes documented below)
facet-ansi, facet-singularize, facet-tokio-postgresNo longer listed



Attribute Quick Reference

Container attributes (struct / enum)

AttributeEffect
deny_unknown_fieldsError on unknown fields during deserialization
defaultUse Default for missing fields
rename_all = "..."Rename all fields/variants using a case convention
transparentForward ser/de to inner type (newtype pattern)
opaqueHide inner structure; use with proxy for ser/de
skip_all_unless_truthyApply skip_unless_truthy to every field
type_tag = "..."Add a type identifier for self-describing formats
crate = pathCustom path to the facet crate (for re-exports)

Enum representation attributes

AttributeFormatExample output
untaggedNo discriminator42 / "hello"
tag = "type"Internal tag as field{"type":"Req","id":1}
tag = "t", content = "c"Adjacent tag+content{"t":"Text","c":"hello"}

Field attributes

AttributeEffect
rename = "..."Rename field in ser/de
default / default = val / default = fn()Default when missing
skipSkip in both ser and de (requires default)
skip_serializingSkip during serialization only
skip_deserializingSkip during deserialization (uses default)
skip_serializing_if = predConditionally skip serialization
skip_unless_truthySkip when value is falsy (see truthiness rules)
sensitiveRedact in debug/pretty output ([REDACTED])
flattenMerge nested struct fields into parent
childMark as child node (KDL, XML)
invariants = fnValidate after deserialization
proxy = TypeUse proxy type for ser/de conversion
opaque, proxy = TypeOpaque + 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

rust
#[derive(Facet)]
#[facet(transparent)]
struct UserId(u64); // Serialized as just the u64

opaque — Hide inner structure

Fields don't need to implement Facet. Cannot be serialized on its own — pair with proxy:

rust
#[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 returns Sameness::Opaque.

skip_unless_truthy — Truthiness rules

TypeTruthy when
booltrue
numbersnon-zero (floats exclude NaN)
Vec, String, slicesnon-empty
OptionSome(_)
arraysnon-zero length

default — Field-level options

rust
#[facet(default)] // Uses Default::default()
#[facet(default = 8080)] // Literal value
#[facet(default = default_timeout())] // Function call

invariants — Post-deserialization validation

Called when partial.build() finalizes a value. Returns bool; false fails deserialization.

rust
#[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

rust
#[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

rust
#[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. This is a direct dividend of the core model: the attributes and field names are still available at runtime, so the error can name them.

rust
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

rust
// 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

rust
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:

rust
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 structure



CLI parsing

Outdated: the sections below document facet-args. The current ecosystem lists figue ("layers CLI args, environment variables, config files and defaults into one typed model") in its place, and its API has not been checked against this note.

Turns any Facet struct into a CLI, with auto-generated help, shell completions and rich error diagnostics.

Attributes

rust
#[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 dispatch

Parsing

rust
use facet_args::from_std_args;

fn main() -> Result<(), Box<dyn std::error::Error>> {
let args: Args = from_std_args()?;
Ok(())
}

Help generation

rust
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

rust
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

Attributes for KDL documents:

rust
#[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 name



Feature flags

Enable in Cargo.toml (see cargo):

toml
[dependencies]
facet = { version = "...", features = ["uuid", "chrono"] }

Third-party crate types

FeatureCrateTypes
uuiduuidUuid
ulidulidUlid
urlurlUrl
chronochronoDateTime<Tz>, NaiveDate, NaiveTime, NaiveDateTime
timetimeDate, Time, PrimitiveDateTime, OffsetDateTime, Duration
jiff02jiffTimestamp, Zoned, DateTime, Date, Time, Span, SignedDuration
caminocaminoUtf8Path, Utf8PathBuf
bytesbytesBytes, BytesMut
ordered-floatordered-floatOrderedFloat<f32/f64>, NotNan<f32/f64>
ruintruintUint<BITS, LIMBS>, Bits<BITS, LIMBS>

Standard library types

FeatureTypes
nonzeroNonZero<T> variants (NonZeroU8, NonZeroI32, …)
netSocketAddr, IpAddr, Ipv4Addr, Ipv6Addr, SocketAddrV4, SocketAddrV6
tuples-12Tuples up to 12 elements (default: 4)
fn-ptrFunction pointer types
docInclude /// doc comments in Shape/Field/Variant (increases binary size)