Attributes
Complete reference for #[facet(...)] attributes.
Container attributes
These attributes apply to structs and enums.
deny_unknown_fields
Produce an error when encountering unknown fields during deserialization. By default, unknown fields are silently ignored.
# [ derive ( Facet )]
# [ facet ( deny_unknown_fields )]
struct Config {
name : String ,
port : u16 ,
} default
Use the type's Default implementation for missing fields during deserialization.
# [ derive ( Facet , Default )]
# [ facet ( default )]
struct Config {
name : String ,
port : u16 , // Will use Default if missing
} rename_all
Rename all fields/variants using a case convention.
# [ derive ( Facet )]
# [ facet ( rename_all = "camelCase" )]
struct Config {
server_name : String , // Serialized as "serverName"
max_connections : u32 , // Serialized as "maxConnections"
} Supported conventions:
"PascalCase""camelCase""snake_case""SCREAMING_SNAKE_CASE""kebab-case""SCREAMING-KEBAB-CASE"
transparent
Forward serialization/deserialization to the inner type. Used for newtype patterns.
# [ derive ( Facet )]
# [ facet ( transparent )]
struct UserId ( u64 ); // Serialized as just the u64 metadata_container
Mark a struct as a metadata container — it serializes transparently through its non-metadata field while preserving metadata for formats that support it.
# [ derive ( Facet )]
# [ facet ( metadata_container )]
struct Documented < T > {
value : T ,
# [ facet ( metadata = "doc" )]
doc : Option < Vec < String >>,
} Rules for metadata containers:
- Exactly one non-metadata field — This is the "value" field that the container serializes as
- At least one metadata field — Fields marked with
#[facet(metadata = "...")] - No duplicate metadata kinds — Each metadata kind can only appear once
Supported metadata kinds:
| Kind | Expected type | Description |
|---|---|---|
"span" | Option<facet_reflect::Span> | Source location (byte offset and length). Span has offset: u32 and len: u32 fields. |
"doc" | Option<Vec<S>> | Documentation comments as lines (without the /// prefix). S can be String, Cow<str>, or any string-like type. |
"tag" | Option<S> | Type tag for formats that support tagged values (like Styx's @string). S can be String, Cow<str>, etc. |
Serialization behavior:
During serialization, the container is transparent — Documented<String> serializes exactly like String. However, formats that support metadata (like Styx) can access the metadata fields and emit them appropriately (e.g., as doc comments).
# [ derive ( Facet )]
# [ facet ( metadata_container )]
struct Documented < T > {
value : T ,
# [ facet ( metadata = "doc" )]
doc : Option < Vec < String >>,
}
# [ derive ( Facet )]
struct Config {
name : Documented < String >,
port : Documented < u16 >,
}
let config = Config {
name : Documented {
value : "myapp" . into (),
doc : Some ( vec! [ "The application name" . into ()]),
},
port : Documented {
value : 8080 ,
doc : Some ( vec! [ "Port to listen on" . into (), "Must be > 1024" . into ()]),
},
};
// JSON (no metadata support): {"name": "myapp", "port": 8080}
// Styx (with metadata support):
// /// The application name
// name "myapp"
// /// Port to listen on
// /// Must be > 1024
// port 8080 Deserialization behavior:
During deserialization, the container is also transparent — the deserializer reads the value field normally and populates metadata fields from format-specific sources:
span— Populated from the parser's position information (byte offset and length in the source)doc— Populated from doc comments in formats that support them (like Styx's///comments)tag— Populated from type tags in formats that support them (like Styx's@stringpatterns)
For formats that don't provide metadata (like JSON), the metadata fields receive their default values (typically None).
// Parsing this Styx input:
// /// The application name
// name "myapp"
// Into this type:
# [ derive ( Facet )]
struct Config {
name : Documented < String >,
}
// Results in:
// Config {
// name: Documented {
// value: "myapp".to_string(),
// doc: Some(vec!["The application name".to_string()]),
// }
// } Composing metadata containers:
You can nest metadata containers to combine multiple kinds of metadata:
# [ derive ( Facet )]
# [ facet ( metadata_container )]
struct Spanned < T > {
value : T ,
# [ facet ( metadata = "span" )]
span : Option < Span >,
}
# [ derive ( Facet )]
# [ facet ( metadata_container )]
struct Documented < T > {
value : T ,
# [ facet ( metadata = "doc" )]
doc : Option < Vec < String >>,
}
// Combine both: value with span AND doc metadata
type FullyAnnotated < T > = Spanned < Documented < T >>;
# [ derive ( Facet )]
struct Schema {
fields : Vec < FullyAnnotated < Field >>,
} When nested, the outer container's value field is the inner container. The metadata from both levels is preserved and accessible to formats that need it.
Why use metadata containers?
- Preserve source information — Track spans, doc comments, or other metadata from parsing
- Format-specific output — Let formats like Styx emit doc comments while JSON ignores them
- Type-safe metadata — The metadata is part of the type system, not a side channel
- Composable — Combine different metadata kinds by nesting containers
Difference from transparent:
transparentrequires exactly one field totalmetadata_containerrequires exactly one non-metadata field, plus one or more metadata fields- Both serialize transparently through their inner value
metadata_containerpreserves metadata for formats that support it
opaque
Mark a type as opaque — its inner structure is hidden from facet. The type itself implements Facet, but its fields are not inspected or serialized. This is useful for:
- Types with fields that don't implement
Facet - Types whose internal structure shouldn't be exposed
- Wrapper types around FFI or unsafe internals
# [ derive ( Facet )]
# [ facet ( opaque )]
struct InternalState {
handle : * mut c_void , // Doesn't need Facet
cache : SomeNonFacetType ,
} Important: Opaque types cannot be serialized or deserialized on their own — use them with #[facet(proxy = ...)] to provide a serializable representation:
// A type that doesn't implement Facet
struct SecretKey ([ u8 ; 32 ]);
// A proxy that can be serialized (as hex string)
# [ derive ( Facet )]
# [ facet ( transparent )]
struct SecretKeyProxy ( String );
impl TryFrom < SecretKeyProxy > for SecretKey {
type Error = &' static str ;
fn try_from ( proxy : SecretKeyProxy ) -> Result < Self , Self :: Error > {
// Parse hex string into bytes
let bytes = hex:: decode ( & proxy. 0 ). map_err ( |_| "invalid hex" ) ?;
let arr: [ u8 ; 32 ] = bytes. try_into (). map_err ( |_| "wrong length" ) ?;
Ok ( SecretKey ( arr))
}
}
impl TryFrom < & SecretKey > for SecretKeyProxy {
type Error = std:: convert:: Infallible ;
fn try_from ( key : & SecretKey ) -> Result < Self , Self :: Error > {
Ok ( SecretKeyProxy ( hex:: encode ( & key. 0 )))
}
}
# [ derive ( Facet )]
struct Config {
name : String ,
# [ facet ( opaque , proxy = SecretKeyProxy )]
key : SecretKey , // Serialized as hex string via proxy
} Note: Field-level #[facet(opaque)] supports borrowed field types via an internal
lifetime-aware wrapper. Direct use of facet::Opaque<T> still requires T: 'static,
which keeps Poke::get_mut sound.
When assert_same! encounters an opaque type, it returns Sameness::Opaque — you cannot structurally compare opaque values.
Opaque adapter (#[facet(opaque = AdapterType)])
For container-level opaque types, you can provide an adapter instead of using proxy. This gives explicit control over how opaque bytes are mapped during serialization and deserialization.
use facet::{ Facet , FacetOpaqueAdapter , OpaqueDeserialize , OpaqueSerialize , PtrConst };
# [ derive ( Facet )]
# [ facet ( opaque = PayloadAdapter )]
struct Payload < ' a >( &' a [ u8 ]);
struct PayloadAdapter ;
impl FacetOpaqueAdapter for PayloadAdapter {
type Error = String ;
type SendValue < ' a > = Payload < ' a >;
type RecvValue < ' de > = Payload < ' de >;
fn serialize_map ( value : & Self :: SendValue < ' _ >) -> OpaqueSerialize {
OpaqueSerialize {
ptr : PtrConst :: new ( & value. 0 as * const & [ u8 ]),
shape : <& [ u8 ] as Facet >:: SHAPE ,
}
}
fn deserialize_build < ' de >(
input : OpaqueDeserialize < ' de >,
) -> Result < Self :: RecvValue < ' de >, Self :: Error > {
Ok ( match input {
OpaqueDeserialize :: Borrowed ( bytes) => Payload ( bytes),
OpaqueDeserialize :: Owned ( _) => {
return Err ( "expected borrowed bytes" . to_string ());
}
})
}
} When forwarding opaque payloads that are already postcard-encoded for postcard,
adapters can return facet_postcard::opaque_encoded_borrowed(bytes) (or
opaque_encoded_owned) instead of a mapped typed value.
Rules and notes:
#[facet(opaque = ...)]is currently container-level only (not supported on fields).- The adapter type must implement
FacetOpaqueAdapter. - This is an alternative to
proxyfor opaque values. #[facet(trailing)]accepts either a field marked#[facet(opaque)]or a field type whose shape has a container-level opaque adapter.
pod
Mark a type as Plain Old Data. POD types have no invariants — any combination of valid field values produces a valid instance. This enables safe mutation through reflection.
# [ derive ( Facet )]
# [ facet ( pod )]
struct Point {
x : i32 ,
y : i32 ,
} What POD means:
- Any combination of valid field values is valid for the struct as a whole
- There are no hidden constraints or relationships between fields
- The type can be safely mutated field-by-field through reflection
What POD does NOT mean:
- POD is not an auto-trait — a struct with all POD fields is not automatically POD
- The type author must explicitly opt in to assert there are no semantic invariants
POD vs invariants: These attributes are mutually exclusive. If you need validation, use invariants; if you want unrestricted mutation, use pod.
// This is an error:
# [ derive ( Facet )]
# [ facet ( pod , invariants = validate )] // ❌ Compile error
struct Invalid { x : i32 } Primitives are implicitly POD: Types like u32, bool, f64, and char are always considered POD — any value of those types is valid.
Containers don't need POD: Vec<T>, Option<T>, and similar containers are manipulated through their vtables, which maintain their own internal invariants. The POD-ness of the element type T matters when mutating elements, not the container itself.
When to use POD:
Use #[facet(pod)] when your type is a simple data container with no semantic constraints:
// Good candidates for POD:
# [ derive ( Facet )]
# [ facet ( pod )]
struct Color { r : u8 , g : u8 , b : u8 }
# [ derive ( Facet )]
# [ facet ( pod )]
struct Dimensions { width : u32 , height : u32 }
// NOT good for POD (has invariant: start <= end):
# [ derive ( Facet )]
# [ facet ( invariants = Range :: is_valid )]
struct Range { start : u32 , end : u32 }
impl Range {
fn is_valid ( & self ) -> bool { self . start <= self . end }
} skip_all_unless_truthy
Applies skip_unless_truthy to every field in the container. This is a convenient shorthand when all or most fields should be omitted if they're falsy.
# [ derive ( Facet )]
# [ facet ( skip_all_unless_truthy )]
struct Config {
name : String , // Omitted if empty
description : String , // Omitted if empty
count : u32 , // Omitted if zero
enabled : bool , // Omitted if false
} Individual fields can still override this with #[facet(skip_serializing)] or by not being marked for skipping.
type_tag
Add a type identifier for self-describing formats.
# [ derive ( Facet )]
# [ facet ( type_tag = "com.example.User" )]
struct User {
name : String ,
} crate
Specify a custom path to the facet crate. This is primarily useful for crates that re-export facet and want users to derive Facet without adding facet as a direct dependency.
// In a crate that re-exports facet
use other_crate:: facet;
# [ derive ( other_crate :: facet :: Facet )]
# [ facet ( crate = other_crate :: facet )]
struct MyStruct {
field : u32 ,
} This attribute can also be used with enums and all struct variants:
use other_crate:: facet;
# [ derive ( other_crate :: facet :: Facet )]
# [ facet ( crate = other_crate :: facet )]
enum MyEnum {
Variant1 ,
Variant2 { data : String },
}
# [ derive ( other_crate :: facet :: Facet )]
# [ facet ( crate = other_crate :: facet )]
struct TupleStruct ( u32 , String ); Enum attributes
These attributes control enum serialization format.
untagged
Serialize enum variants without a discriminator tag. The deserializer tries each variant in order until one succeeds.
# [ derive ( Facet )]
# [ facet ( untagged )]
enum Value {
Int ( i64 ),
Float ( f64 ),
String ( String ),
} Variant matching order
For formats with typed values (JSON, YAML, MessagePack), variants are tried in definition order. The first matching variant wins. In the example above, a JSON 42 matches Int(i64) because integers match i64.
Text-based formats (XML)
Text-based formats like XML represent all values as strings. When deserializing <value>42</value>, the parser produces a "stringly-typed" value — text that may encode a more specific type.
For stringly-typed values, facet uses two-tier matching:
Tier 1 (Parseable types): Try non-string types that can parse the value (
i64,f64,bool, etc.). First successful parse wins, in definition order.Tier 2 (String fallback): If no parseable type matched, fall back to
String/&str/Cow<str>variants.
This ensures <value>42</value> matches Int(i64) rather than String(String), regardless of definition order:
# [ derive ( Facet )]
# [ facet ( untagged )]
enum Value {
Text ( String ), // Tier 2: tried last for stringly-typed values
Number ( i64 ), // Tier 1: tried first (parses "42")
Flag ( bool ), // Tier 1: tried first (doesn't parse "42")
}
// XML: <v>42</v> → Number(42)
// XML: <v>true</v> → Flag(true)
// XML: <v>hello</v> → Text("hello") tag
Use internal tagging — the variant name becomes a field inside the object.
# [ derive ( Facet )]
# [ facet ( tag = "type" )]
enum Message {
Request { id : u64 , method : String },
Response { id : u64 , result : String },
}
// {"type": "Request", "id": 1, "method": "get"} tag + content
Use adjacent tagging — separate fields for the tag and content.
# [ derive ( Facet )]
# [ facet ( tag = "t" , content = "c" )]
enum Message {
Text ( String ),
Data ( Vec < u8 >),
}
// {"t": "Text", "c": "hello"} Variant attributes
These attributes apply to enum variants.
other
Mark a variant as a catch-all for unknown variant names. When deserializing, if no variant matches the input tag, the variant marked with other will be used.
# [ derive ( Facet )]
enum Status {
Active ,
Inactive ,
# [ facet ( other )]
Unknown ( String ), // Catches "Pending", "Archived", etc.
} Capturing variant tags with #[facet(tag)] and #[facet(content)]
For self-describing formats that emit VariantTag events (like Styx or XML), an #[facet(other)] variant can capture both the tag name and its payload using field-level #[facet(tag)] and #[facet(content)] attributes:
# [ derive ( Facet )]
enum Schema {
// Known variants
Object ( ObjectSchema ),
Seq ( SeqSchema ),
// Catch-all for unknown tags like @string, @unit, @MyCustomType
# [ facet ( other )]
Type {
# [ facet ( tag )]
name : String , // Captures the tag name (e.g., "string", "unit")
# [ facet ( content )]
payload : Value , // Captures the payload
},
} With this definition:
@object{...}→Schema::Object(...)(known variant)@string→Schema::Type { name: "string", payload: Value::Unit }@custom"data"→Schema::Type { name: "custom", payload: Value::Str("data") }@foo{x 1}→Schema::Type { name: "foo", payload: Value::Object(...) }
Rules for #[facet(other)] variants:
- Only one variant per enum can be marked
#[facet(other)] - When used with
VariantTagevents:- A field with
#[facet(tag)]receives the tag name as aString - A field with
#[facet(content)]receives the deserialized payload - If no
#[facet(content)]field exists, the payload must be unit (empty)
- A field with
- For non-self-describing formats (JSON with external tagging), the variant name from the input is used directly
Discarding payloads:
If you only care about the tag name and know the payload is always unit, you can omit the #[facet(content)] field:
# [ derive ( Facet )]
enum TypeRef {
Object ,
Seq ,
# [ facet ( other )]
Named {
# [ facet ( tag )]
name : String , // Just capture the tag name, payload must be unit
},
} This will fail deserialization if the unknown variant has a non-unit payload.
Field attributes
These attributes apply to struct fields.
rename
Rename a field during serialization/deserialization.
# [ derive ( Facet )]
struct User {
# [ facet ( rename = "user_name" )]
name : String ,
} default
Use a default value when the field is missing during deserialization.
# [ derive ( Facet )]
struct Config {
name : String ,
# [ facet ( default )] // Uses Default::default()
tags : Vec < String >,
# [ facet ( default = 8080 )] // Uses literal value
port : u16 ,
# [ facet ( default = default_timeout ())] // Uses function
timeout : Duration ,
}
fn default_timeout () -> Duration {
Duration :: from_secs ( 30 )
} skip
Skip this field entirely during both serialization and deserialization. The field must have a default value.
# [ derive ( Facet )]
struct Session {
id : String ,
# [ facet ( skip , default )]
internal_state : InternalState ,
} skip_serializing
Skip this field during serialization only.
# [ derive ( Facet )]
struct User {
name : String ,
# [ facet ( skip_serializing )]
password_hash : String ,
} skip_deserializing
Skip this field during deserialization (uses default value).
# [ derive ( Facet )]
struct Record {
data : String ,
# [ facet ( skip_deserializing , default )]
computed_field : i32 ,
} skip_serializing_if
Conditionally skip serialization based on a predicate.
# [ derive ( Facet )]
struct User {
name : String ,
# [ facet ( skip_serializing_if = Option :: is_none )]
email : Option < String >,
# [ facet ( skip_serializing_if = Vec :: is_empty )]
tags : Vec < String >,
# [ facet ( skip_serializing_if = |n| * n == 0 )]
count : i32 ,
} skip_unless_truthy
Conditionally skip serialization unless the value is truthy. Uses the type's registered truthiness predicate.
Truthiness is evaluated based on the type:
- Booleans:
trueis truthy,falseis falsy - Numbers: non-zero is truthy (for floats, also excludes NaN)
- Collections (Vec, String, slice, etc.): non-empty is truthy
- Option:
Some(_)is truthy,Noneis falsy - Arrays: non-zero-length arrays are truthy
# [ derive ( Facet )]
struct User {
name : String ,
# [ facet ( skip_unless_truthy )]
email : Option < String >, // Omitted if None
# [ facet ( skip_unless_truthy )]
tags : Vec < String >, // Omitted if empty
# [ facet ( skip_unless_truthy )]
bio : String , // Omitted if empty
} This is more ergonomic than skip_serializing_if when the type already has a natural notion of truthiness.
sensitive
Mark a field as containing sensitive data. Tools like facet-pretty will redact this field in debug output.
# [ derive ( Facet )]
struct Config {
name : String ,
# [ facet ( sensitive )]
api_key : String , // Shown as [REDACTED] in debug output
} flatten
Flatten a nested struct's fields into the parent.
# [ derive ( Facet )]
struct Pagination {
page : u32 ,
per_page : u32 ,
}
# [ derive ( Facet )]
struct Query {
search : String ,
# [ facet ( flatten )]
pagination : Pagination ,
}
// Serializes as: {"search": "...", "page": 1, "per_page": 10} Flatten with internally-tagged enums:
You can use #[facet(flatten)] inside variants of internally-tagged enums. The flattened fields are merged with the variant's own fields:
# [ derive ( Facet )]
struct Base {
name : String ,
value : i32 ,
}
# [ derive ( Facet )]
# [ facet ( tag = "type" )]
# [ repr ( C )]
enum Message {
# [ facet ( rename = "request" )]
Request {
# [ facet ( flatten )]
base : Base ,
method : String ,
},
# [ facet ( rename = "response" )]
Response {
# [ facet ( flatten )]
base : Base ,
},
}
// Request serializes as:
// {"type": "request", "name": "...", "value": 42, "method": "GET"}
//
// Response serializes as:
// {"type": "response", "name": "...", "value": 42} This pattern is useful for sharing common fields across enum variants while keeping the JSON structure flat.
trailing
Mark an opaque field as structurally trailing in its container. Formats that support trailing payloads can treat this field as "remaining bytes" rather than requiring an outer length frame.
# [ derive ( Facet )]
struct Packet {
tag : u8 ,
len : u16 ,
# [ facet ( opaque , trailing )]
payload : Vec < u8 >,
} #[facet(trailing)] is checked at compile time and must satisfy all of these rules:
- It does not accept arguments (
#[facet(trailing)], not#[facet(trailing = ...)]). - The field must be the last field in its container.
- The field cannot also be
#[facet(flatten)]. - The field must be opaque, either directly (
#[facet(opaque)]) or via its field type.
child
Mark a field as a child node for hierarchical formats like XML.
# [ derive ( Facet )]
struct Document {
title : String ,
# [ facet ( child )]
sections : Vec < Section >,
} metadata
Mark a field as carrying metadata about the value, not part of the value itself. Used with #[facet(metadata_container)] to create transparent wrappers that preserve metadata.
# [ derive ( Facet )]
# [ facet ( metadata_container )]
struct Spanned < T > {
value : T ,
# [ facet ( metadata = "span" )]
span : Option < Span >,
}
# [ derive ( Facet )]
# [ facet ( metadata_container )]
struct Documented < T > {
value : T ,
# [ facet ( metadata = "doc" )]
doc : Option < Vec < String >>,
} The metadata kind string identifies what type of metadata this field carries:
"span"— Source location information (line, column, byte offset)"doc"— Documentation comments from the source- Custom kinds for format-specific metadata
How formats use metadata:
Formats can query metadata fields during serialization/deserialization:
// In a format's serializer:
if let Some ( doc_field) = struct_def. fields . iter ()
. find ( |f| f. metadata_kind () == Some ( "doc" ))
{
// Access the doc field's value and emit it appropriately
// e.g., as doc comments in Styx: /// line 1\n/// line 2
} Metadata fields are not serialized in the normal field list — they're either:
- Handled specially by formats that understand them (e.g., Styx emits doc comments)
- Ignored by formats that don't support metadata (e.g., JSON)
See metadata_container for complete usage examples.
invariants
Validate type invariants after deserialization. The function takes &self and returns bool — returning false causes deserialization to fail.
# [ derive ( Facet )]
# [ facet ( invariants = validate_port )]
struct ServerConfig {
port : u16 ,
}
fn validate_port ( config : & ServerConfig ) -> bool {
config. port > 0 && config. port < 65535
} When is it called? The invariant function is called when finalizing a Partial value — that is, when partial.build() is called after all fields have been set. At this point, the entire value is initialized and can be validated as a whole.
Method syntax: You can also use a method on the type itself:
# [ derive ( Facet )]
# [ facet ( invariants = Point :: is_valid )]
struct Point {
x : i32 ,
y : i32 ,
}
impl Point {
fn is_valid ( & self ) -> bool {
// Point must be in first quadrant
self . x >= 0 && self . y >= 0
}
} Multi-field invariants: This is where invariants really shine — validating relationships between fields:
# [ derive ( Facet )]
# [ facet ( invariants = Range :: is_valid )]
struct Range {
min : u32 ,
max : u32 ,
}
impl Range {
fn is_valid ( & self ) -> bool {
self . min <= self . max
}
} With enums: Enums themselves don't support invariants directly, but you can wrap them in a struct:
# [ derive ( Facet )]
# [ repr ( C )]
enum RangeKind {
Low ( u8 ),
High ( u8 ),
}
# [ derive ( Facet )]
# [ facet ( invariants = ValidatedRange :: is_valid )]
struct ValidatedRange {
range : RangeKind ,
}
impl ValidatedRange {
fn is_valid ( & self ) -> bool {
match & self . range {
RangeKind :: Low ( v) => * v <= 50 ,
RangeKind :: High ( v) => * v > 50 ,
}
}
} Why this matters: Invariants are crucial for types where certain field combinations are invalid. Without them, deserialization could produce values that violate your type's assumptions, potentially leading to logic errors or — in unsafe code — undefined behavior.
Current limitation: Invariants are only checked at the top level when building a Partial. Nested structs with their own invariants are not automatically validated when contained in a parent struct. If you need nested validation, add an invariant to the parent that explicitly checks nested values.
proxy
Use a proxy type for serialization/deserialization. The proxy type handles the format representation while your actual type handles the domain logic.
Required trait implementations:
TryFrom<ProxyType> for FieldType— for deserialization (proxy → actual)TryFrom<&FieldType> for ProxyType— for serialization (actual → proxy)
use facet:: Facet ;
// Your domain type
struct CustomId ( u64 );
// Proxy: serialize as a string with "ID-" prefix
# [ derive ( Facet )]
# [ facet ( transparent )]
struct CustomIdProxy ( String );
impl TryFrom < CustomIdProxy > for CustomId {
type Error = &' static str ;
fn try_from ( proxy : CustomIdProxy ) -> Result < Self , Self :: Error > {
let num = proxy. 0 . strip_prefix ( "ID-" )
. ok_or ( "missing ID- prefix" ) ?
. parse ()
. map_err ( |_| "invalid number" ) ?;
Ok ( CustomId ( num))
}
}
impl TryFrom < & CustomId > for CustomIdProxy {
type Error = std:: convert:: Infallible ;
fn try_from ( id : & CustomId ) -> Result < Self , Self :: Error > {
Ok ( CustomIdProxy ( format! ( "ID-{}" , id. 0 )))
}
}
# [ derive ( Facet )]
struct Record {
# [ facet ( proxy = CustomIdProxy )]
id : CustomId ,
}
// Serialization: actual type → proxy → JSON
let record = Record { id : CustomId ( 12345 ) };
let json = facet_json:: to_string ( & record);
assert_eq! ( json, r#"{"id":"ID-12345"}"# );
// Deserialization: JSON → proxy → actual type
let parsed: Record = facet_json:: from_str ( & json). unwrap ();
assert_eq! ( parsed. id . 0 , 12345 ); Use cases for proxy:
- Custom serialization format — serialize numbers as strings, dates as timestamps, etc.
- Type conversion — deserialize a string into a parsed type using
FromStr - Validation — reject invalid values during
TryFromconversion - Non-Facet types — combine with
#[facet(opaque)]for types that don't implementFacet
Example: Delegate to FromStr and Display
A common pattern is parsing string fields using a type's FromStr implementation. For example, parsing "#ff00ff" into a color struct:
use facet:: Facet ;
use std:: str:: FromStr ;
/// A color type that can be parsed from hex strings like "#ff00ff"
# [ derive ( Debug , PartialEq )]
struct Color ( u8 , u8 , u8 );
impl FromStr for Color {
type Err = String ;
fn from_str ( s : & str ) -> Result < Self , Self :: Err > {
let s = s. strip_prefix ( '#' ). unwrap_or ( s);
if s. len () != 6 {
return Err ( "expected 6 hex digits" . into ());
}
let r = u8:: from_str_radix ( & s[ 0 ..2 ], 16 ). map_err ( |e| e. to_string ()) ?;
let g = u8:: from_str_radix ( & s[ 2 ..4 ], 16 ). map_err ( |e| e. to_string ()) ?;
let b = u8:: from_str_radix ( & s[ 4 ..6 ], 16 ). map_err ( |e| e. to_string ()) ?;
Ok ( Color ( r, g, b))
}
}
impl std:: fmt:: Display for Color {
fn fmt ( & self , f : & mut std:: fmt:: Formatter < ' _ >) -> std:: fmt:: Result {
write! ( f, "#{:02x}{:02x}{:02x}" , self . 0 , self . 1 , self . 2 )
}
}
// Step 1: Create a transparent proxy that wraps String
# [ derive ( Facet )]
# [ facet ( transparent )]
struct ColorProxy ( String );
// Step 2: Implement TryFrom for deserialization (Proxy → Color)
impl TryFrom < ColorProxy > for Color {
type Error = String ;
fn try_from ( proxy : ColorProxy ) -> Result < Self , Self :: Error > {
Color :: from_str ( & proxy. 0 ) // Delegate to FromStr
}
}
// Step 3: Implement TryFrom for serialization (Color → Proxy)
impl TryFrom < & Color > for ColorProxy {
type Error = std:: convert:: Infallible ;
fn try_from ( color : & Color ) -> Result < Self , Self :: Error > {
Ok ( ColorProxy ( color. to_string ())) // Delegate to Display
}
}
// Step 4: Use the proxy attribute on your field
# [ derive ( Facet )]
struct Theme {
# [ facet ( proxy = ColorProxy )]
foreground : Color ,
# [ facet ( proxy = ColorProxy )]
background : Color ,
}
// Serialization works in both directions:
let theme = Theme {
foreground : Color ( 255 , 0 , 255 ),
background : Color ( 0 , 0 , 0 ),
};
let json = facet_json:: to_string ( & theme);
assert_eq! ( json, r#"{"foreground":"# ff00ff" , " background" : " #000000 " } " #);
// And deserialization:
let parsed: Theme = facet_json:: from_str ( & json). unwrap ();
assert_eq! ( parsed. foreground , Color ( 255 , 0 , 255 )); The key insight: #[facet(transparent)] on the proxy makes it serialize as just a string (not {"0": "..."}), and the TryFrom impls handle the conversion in both directions.
Example: Parse integers from hex strings:
# [ derive ( Facet )]
# [ facet ( transparent )]
struct HexU64 ( String );
impl TryFrom < HexU64 > for u64 {
type Error = std:: num:: ParseIntError ;
fn try_from ( proxy : HexU64 ) -> Result < Self , Self :: Error > {
let s = proxy. 0 . strip_prefix ( "0x" ). unwrap_or ( & proxy. 0 );
u64:: from_str_radix ( s, 16 )
}
}
impl TryFrom < & u64 > for HexU64 {
type Error = std:: convert:: Infallible ;
fn try_from ( n : & u64 ) -> Result < Self , Self :: Error > {
Ok ( HexU64 ( format! ( "0x{:x}" , n)))
}
}
# [ derive ( Facet )]
struct Pointer {
# [ facet ( proxy = HexU64 )]
address : u64 ,
}
// Serialization: the address is formatted as hex
let ptr = Pointer { address : 0x7fff5fbff8c0 };
let json = facet_json:: to_string ( & ptr);
assert_eq! ( json, r#"{"address":"0x7fff5fbff8c0"}"# );
// Deserialization: hex string is parsed back to u64
let parsed: Pointer = facet_json:: from_str ( & json). unwrap ();
assert_eq! ( parsed. address , 0x7fff5fbff8c0 ); Example: Nested proxy with opaque type:
// Arc<T> with a custom serialization
# [ derive ( Facet )]
struct ArcU64Proxy { val : u64 }
impl TryFrom < ArcU64Proxy > for std:: sync:: Arc < u64 > {
type Error = std:: convert:: Infallible ;
fn try_from ( proxy : ArcU64Proxy ) -> Result < Self , Self :: Error > {
Ok ( std:: sync:: Arc :: new ( proxy. val ))
}
}
impl TryFrom < & std:: sync:: Arc < u64 >> for ArcU64Proxy {
type Error = std:: convert:: Infallible ;
fn try_from ( arc : & std:: sync:: Arc < u64 >) -> Result < Self , Self :: Error > {
Ok ( ArcU64Proxy { val : ** arc })
}
}
# [ derive ( Facet )]
struct Container {
# [ facet ( opaque , proxy = ArcU64Proxy )]
counter : std:: sync:: Arc < u64 >,
}
// Serialization: Arc<u64> → ArcU64Proxy → JSON object
let container = Container { counter : std:: sync:: Arc :: new ( 42 ) };
let json = facet_json:: to_string ( & container);
assert_eq! ( json, r#"{"counter":{"val":42}}"# );
// Deserialization: JSON object → ArcU64Proxy → Arc<u64>
let parsed: Container = facet_json:: from_str ( & json). unwrap ();
assert_eq! ( * parsed. counter , 42 ); Format-specific proxies
Sometimes a type needs different serialization representations for different formats. For example, you might want hex strings in JSON but binary strings in a custom format.
Use the format namespace syntax: #[facet(json::proxy = JsonProxy)], #[facet(xml::proxy = XmlProxy)], etc.
Resolution order:
- Format-specific proxy (e.g.,
json::proxywhen serializing JSON) - Format-agnostic proxy (
proxy) - Normal serialization (no proxy)
use facet:: Facet ;
/// Proxy for JSON: serialize as hex string
# [ derive ( Facet )]
# [ facet ( transparent )]
struct HexProxy ( String );
impl TryFrom < HexProxy > for u32 {
type Error = std:: num:: ParseIntError ;
fn try_from ( proxy : HexProxy ) -> Result < Self , Self :: Error > {
let s = proxy. 0 . trim_start_matches ( "0x" );
u32:: from_str_radix ( s, 16 )
}
}
impl From < & u32 > for HexProxy {
fn from ( v : & u32 ) -> Self {
HexProxy ( format! ( "0x{:x}" , v))
}
}
/// Proxy for other formats: serialize as decimal string
# [ derive ( Facet )]
# [ facet ( transparent )]
struct DecimalProxy ( String );
impl TryFrom < DecimalProxy > for u32 {
type Error = std:: num:: ParseIntError ;
fn try_from ( proxy : DecimalProxy ) -> Result < Self , Self :: Error > {
proxy. 0 . parse ()
}
}
impl From < & u32 > for DecimalProxy {
fn from ( v : & u32 ) -> Self {
DecimalProxy ( v. to_string ())
}
}
# [ derive ( Facet )]
struct Config {
name : String ,
# [ facet ( json :: proxy = HexProxy )] // Use hex in JSON
# [ facet ( proxy = DecimalProxy )] // Use decimal elsewhere
port : u32 ,
}
// JSON serialization uses hex:
// {"name":"app","port":"0x1f90"}
// Other formats use decimal:
// name: app
// port: "8080" Use cases:
- Different encoding requirements per format (hex vs binary vs base64)
- XML attributes need strings, JSON can use native types
- Legacy format compatibility with different representations
Note: For a format to support format-specific proxies, its parser/serializer must implement format_namespace() to return its namespace. The following built-in format crates support this:
facet-json→"json"(use#[facet(json::proxy = ...)])facet-xml→"xml"(use#[facet(xml::proxy = ...)])facet-html→"html"(use#[facet(html::proxy = ...)])
Extension attributes
Format crates can define their own namespaced attributes with compile-time validation and helpful error messages.
Using extension attributes
The namespace comes from how you import the crate:
use facet:: Facet ;
use facet_xml as xml;
use figue as args;
# [ derive ( Facet )]
struct Config {
# [ facet ( xml :: element )]
server : Server ,
# [ facet ( xml :: attribute )]
name : String ,
}
# [ derive ( Facet )]
struct Cli {
# [ facet ( args :: positional )]
input : String ,
# [ facet ( args :: named , args :: short = 'o' )]
output : Option < String >,
} Available namespaces
| Crate | Namespace | Attributes |
|---|---|---|
figue | args | positional, named, short, subcommand |
facet-xml | xml | element, elements, attribute, text, tag, ns, ns_all, proxy |
facet-html | html | element, elements, attribute, text, tag, custom_element, proxy |
facet-yaml | serde | rename |
facet-json | json | proxy |
Note: The proxy attribute is available for any format namespace (e.g., json::proxy, xml::proxy). It uses the same syntax as the format-agnostic proxy attribute but only applies when serializing/deserializing with that specific format.
Creating your own
Use define_attr_grammar!:
facet:: define_attr_grammar! {
ns "myformat" ;
crate_path :: my_format_crate;
pub enum Attr {
/// Description shown in error messages
MyAttribute ,
/// Attribute with a value
WithValue ( &' static str ),
}
} Typos produce helpful compile errors:
error: unknown attribute `chld`, did you mean `child`?
available attributes: child, children, property, argumentSee Extend → Extension Attributes for the complete guide.