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)

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

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

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

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

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

facet-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 dispatch

Parsing

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 name

Ecosystem Integration

Third-party crate features

Enable in Cargo.toml:

[dependencies]
facet = { version = "...", features = ["uuid", "chrono"] }
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 features

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)

Crate Index

CrateDescription
facetCore derive macro + trait
facet-jsonJSON format
facet-yamlYAML format
facet-tomlTOML format
facet-kdlKDL format
facet-xmlXML format
facet-csvCSV format
facet-msgpackMessagePack format
facet-urlencodedURL-encoded format
facet-argsCLI argument parsing
facet-valueDynamic Value type
facet-prettyPretty-printing
facet-diffStructural diffing
facet-validateValidation
facet-errorError types
facet-ansiANSI terminal output
facet-svgSVG output
facet-htmlHTML output
facet-html-domHTML DOM
facet-json-schemaJSON Schema generation
facet-axumAxum integration
facet-tokio-postgrestokio-postgres integration
facet-singularizeWord singularization (used by KDL)