facet-rs: idiomatic facet ecosystem usage
Overview
facet (https://facet.rs) is a reflection library: #[derive(Facet)] generates a &'static Shape describing the type once, and format crates (facet-json, facet-csv, ...) interpret that shape at runtime. One derive powers serialization, pretty-printing, diffing, CLI parsing, and schema generation. It trades raw speed for features — serde monomorphizes per type×format and is faster in hot loops.
Verified against facet 0.46.5 / facet-json 0.46.1 / strid 10.0.0 (2026-07-12).
Critical gotchas (memorize these)
- Enums require an explicit repr — but that's memory-only, not the wire form.
#[derive(Facet)] on an enum needs #[repr(u8)]/#[repr(C)] or it's a compile error ("Facet requires enums to have an explicit representation"). This does not serialize as an integer: unit (dataless) variants serialize as their variant-name string (Kind::Fee → "Fee"), like serde; use #[facet(rename_all = "kebab-case")] to control casing ("card-payment"). Never read #[repr(u8)] as "serialize as integer".
rust_decimal::Decimal implements Facet natively — enable the rust_decimal feature on facet. It serializes as a JSON string with scale preserved ("1234.560"). Deserializing from a JSON string is lossless (28 digits); a bare JSON number silently truncates through f64 (~17 significant digits). Keep money as strings on the wire. No newtype or hand-written impl needed. For a foreign, serde-based money type, use the pure-safe field-level proxy — see "THE MONEY PATTERNS" in references/type-support-and-custom-impls.md.
Option<T> implicitly defaults to None when missing; Vec/HashMap do NOT in released 0.46.x — a missing collection field is a hard error despite the serde-comparison page claiming otherwise. Write #[facet(default)] on every collection field that may be absent.
- strid validated braids are NOT validated by facet deserialization.
#[braid(serde, validator)] enforces the validator under serde only; facet's derived impl is transparent and bypasses Validator::validate. Re-validate after facet deserialization or accept raw String at the boundary and convert with Type::new().
- Always put
#[facet(deny_unknown_fields)] on structs that parse external input. Unknown fields are silently ignored by default.
- facet-csv is one-record-per-call and headerless.
from_str parses a single row; serializing a Vec<Row> fails with "CSV does not support sequences". Iterate lines yourself.
facet_json::to_string returns Result — the website getting-started sample that omits .unwrap() is stale.
- facet-validate constraints ARE enforced during facet-json deserialization — errors like
Validation failed for field 'T::count': must be >= 1, got 0 come back with input spans.
#[derive(Facet)] expands to unsafe impl Facet — incompatible with crate-wide forbid(unsafe_code). Step down to unsafe_code = "deny" + targeted #[expect(unsafe_code, reason = "…")]; see the unsafe ladder in the rust-best-practices skill's references/lint-setup.md.
Cargo setup
[dependencies]
facet = { version = "0.46", features = ["rust_decimal", "chrono", "uuid"] }
facet-json = "0.46"
facet-csv = "0.46" # if reading/writing CSV
facet-error = "0.46" # thiserror-style derives
facet-validate = "0.46" # field constraints
rediff = "0.46" # structural diff assertions (dev-dependency)
strid = "10" # braids (re-exports facet)
figue = "4" # CLI/config (if building a binary)
Format crates version independently of facet (0.46.1 vs 0.46.5 is normal). default features on facet include std, helpful-derive (typo suggestions), and doc (doc comments in shapes — figue help text needs this).
Core idiom
use facet::Facet;
use rust_decimal::Decimal;
#[derive(Facet, Debug, PartialEq)]
#[facet(deny_unknown_fields)]
struct Transaction {
date: chrono::NaiveDate, // "2026-01-15"
amount: Decimal, // "12.34" — string in JSON, scale preserved
payee: String,
note: Option<String>, // missing → None (implicit)
#[facet(default)] // REQUIRED for collections that may be absent
tags: Vec<String>,
}
let t: Transaction = facet_json::from_str(json)?; // spans in errors
let out = facet_json::to_string(&t)?; // compact; _pretty for indented
What to load when
| Task |
Reference file |
Any #[facet(...)] attribute; porting serde attributes |
references/attributes.md |
| facet-json API, JSONC, error spans; facet-csv rows; TOML/YAML; format support matrix |
references/formats-json-csv.md |
| The money patterns (native Decimal, foreign-type proxy, safety tiering); which types implement Facet (Uuid, chrono...); proxy/opaque for foreign types |
references/type-support-and-custom-impls.md |
| Defining/using strid braids; validation; serde bridging |
references/strid-braids.md |
| CLI args, env vars, layered config with figue |
references/figue-cli.md |
| Error enums (facet-error), validation (facet-validate, invariants), defaults (facet-default) |
references/errors-and-validation.md |
| Round-trip tests, rediff assertions, correctness conventions |
references/testing-with-rediff.md |
For choosing between facet and serde as a stack, see the serde-vs-facet stack table at the head of the rust-best-practices skill's references/serde.md.
1---2name: facet3description: Use when writing Rust code with the facet ecosystem - derive(Facet), facet-json/facet-csv serialization, figue CLI/config, facet-error derives, facet-validate constraints, strid braids, rediff structural diffs, or serializing rust_decimal::Decimal/jiff/chrono types under facet. Serde-to-facet attribute mapping, Decimal/money patterns, and gotchas verified against facet 0.46.4---56# facet-rs: idiomatic facet ecosystem usage78## Overview910facet (https://facet.rs) is a reflection library: `#[derive(Facet)]` generates a `&'static Shape` describing the type once, and format crates (facet-json, facet-csv, ...) interpret that shape at runtime. One derive powers serialization, pretty-printing, diffing, CLI parsing, and schema generation. It trades raw speed for features — serde monomorphizes per type×format and is faster in hot loops.1112Verified against facet 0.46.5 / facet-json 0.46.1 / strid 10.0.0 (2026-07-12).1314## Critical gotchas (memorize these)15161. **Enums require an explicit repr — but that's memory-only, not the wire form.** `#[derive(Facet)]` on an enum needs `#[repr(u8)]`/`#[repr(C)]` or it's a compile error ("Facet requires enums to have an explicit representation"). This does **not** serialize as an integer: unit (dataless) variants serialize as their **variant-name string** (`Kind::Fee` → `"Fee"`), like serde; use `#[facet(rename_all = "kebab-case")]` to control casing (`"card-payment"`). Never read `#[repr(u8)]` as "serialize as integer".172. **`rust_decimal::Decimal` implements `Facet` natively** — enable the `rust_decimal` feature on `facet`. It serializes as a **JSON string with scale preserved** (`"1234.560"`). Deserializing from a JSON **string is lossless** (28 digits); a bare JSON **number silently truncates through f64** (~17 significant digits). Keep money as strings on the wire. **No newtype or hand-written impl needed.** For a foreign, serde-based money type, use the pure-safe field-level proxy — see "THE MONEY PATTERNS" in `references/type-support-and-custom-impls.md`.183. **`Option<T>` implicitly defaults to `None` when missing; `Vec`/`HashMap` do NOT** in released 0.46.x — a missing collection field is a hard error despite the serde-comparison page claiming otherwise. Write `#[facet(default)]` on every collection field that may be absent.194. **strid validated braids are NOT validated by facet deserialization.** `#[braid(serde, validator)]` enforces the validator under serde only; facet's derived impl is transparent and bypasses `Validator::validate`. Re-validate after facet deserialization or accept raw `String` at the boundary and convert with `Type::new()`.205. **Always put `#[facet(deny_unknown_fields)]` on structs that parse external input.** Unknown fields are silently ignored by default.216. **facet-csv is one-record-per-call and headerless.** `from_str` parses a single row; serializing a `Vec<Row>` fails with "CSV does not support sequences". Iterate lines yourself.227. **`facet_json::to_string` returns `Result`** — the website getting-started sample that omits `.unwrap()` is stale.238. **facet-validate constraints ARE enforced during facet-json deserialization** — errors like `Validation failed for field 'T::count': must be >= 1, got 0` come back with input spans.249. **`#[derive(Facet)]` expands to `unsafe impl Facet`** — incompatible with crate-wide `forbid(unsafe_code)`. Step down to `unsafe_code = "deny"` + targeted `#[expect(unsafe_code, reason = "…")]`; see the unsafe ladder in the `rust-best-practices` skill's `references/lint-setup.md`.2526## Cargo setup2728```toml29[dependencies]30facet = { version = "0.46", features = ["rust_decimal", "chrono", "uuid"] }31facet-json = "0.46"32facet-csv = "0.46" # if reading/writing CSV33facet-error = "0.46" # thiserror-style derives34facet-validate = "0.46" # field constraints35rediff = "0.46" # structural diff assertions (dev-dependency)36strid = "10" # braids (re-exports facet)37figue = "4" # CLI/config (if building a binary)38```3940Format crates version independently of `facet` (0.46.1 vs 0.46.5 is normal). `default` features on `facet` include `std`, `helpful-derive` (typo suggestions), and `doc` (doc comments in shapes — figue help text needs this).4142## Core idiom4344```rust45use facet::Facet;46use rust_decimal::Decimal;4748#[derive(Facet, Debug, PartialEq)]49#[facet(deny_unknown_fields)]50struct Transaction {51 date: chrono::NaiveDate, // "2026-01-15"52 amount: Decimal, // "12.34" — string in JSON, scale preserved53 payee: String,54 note: Option<String>, // missing → None (implicit)55 #[facet(default)] // REQUIRED for collections that may be absent56 tags: Vec<String>,57}5859let t: Transaction = facet_json::from_str(json)?; // spans in errors60let out = facet_json::to_string(&t)?; // compact; _pretty for indented61```6263## What to load when6465| Task | Reference file |66|---|---|67| Any `#[facet(...)]` attribute; porting serde attributes | `references/attributes.md` |68| facet-json API, JSONC, error spans; facet-csv rows; TOML/YAML; format support matrix | `references/formats-json-csv.md` |69| **The money patterns** (native Decimal, foreign-type proxy, safety tiering); which types implement Facet (Uuid, chrono...); proxy/opaque for foreign types | `references/type-support-and-custom-impls.md` |70| Defining/using strid braids; validation; serde bridging | `references/strid-braids.md` |71| CLI args, env vars, layered config with figue | `references/figue-cli.md` |72| Error enums (facet-error), validation (facet-validate, invariants), defaults (facet-default) | `references/errors-and-validation.md` |73| Round-trip tests, rediff assertions, correctness conventions | `references/testing-with-rediff.md` |7475For choosing between facet and serde as a stack, see the serde-vs-facet stack table at the head of the `rust-best-practices` skill's `references/serde.md`.