facet-solver
Helps facet deserializers implement #[facet(flatten)] and #[facet(untagged)]
correctly, efficiently, and with useful diagnostics.
The Problem
When deserializing a type with a flattened enum:
# use Facet;
...we don't know which variant to use until we've seen the fields:
// Text
// Binary
The solver answers: "which variant has a content field?" or "which variant
has both data and encoding?"
How It Works
The solver pre-computes all valid field combinations ("configurations") for a type, then uses an inverted index to quickly find which configuration(s) match the fields you've seen.
use ;
# use Facet;
#
#
#
#
#
#
#
#
#
// Build schema once (can be cached)
let schema = build.unwrap;
// Create a solver for this deserialization
let mut solver = new;
// As you see fields, report them:
match solver.see_key
match solver.see_key
Nested Disambiguation
When top-level keys don't distinguish variants, the solver can look deeper:
# use Facet;
Both variants have an inner field. But inner.content only exists in Text,
and inner.bytes only exists in Binary. The ProbingSolver handles this:
use ;
# use Facet;
#
#
#
#
#
#
#
#
#
let schema = build.unwrap;
let mut solver = new;
// Top-level "inner" doesn't disambiguate
assert!;
// But "inner.content" does!
match solver.probe_key
Lazy Type Disambiguation
Sometimes variants have identical keys but different value types. The solver handles this without buffering—it lets you probe "can this value fit type X?" lazily:
# use Facet;
Both variants have payload.value, but one is u8 (max 255) and one is u16 (max 65535).
When the deserializer sees value 1000, it can rule out Small without ever parsing into
the wrong type:
use ;
# use Facet;
#
#
#
#
#
#
#
#
#
let schema = build.unwrap;
let mut solver = new;
// "payload" exists in both - ambiguous by key alone
solver.probe_key;
// "value" also exists in both, but with different types!
match solver.probe_key
// Deserializer sees value 1000 - ask which types fit
let shapes = solver.get_shapes_at_path;
let fits: = shapes.iter
.filter
.copied
.collect;
// Narrow to types the value actually fits
solver.satisfy_at_path;
assert_eq!; // Solved: Large
This enables true streaming deserialization: you never buffer values, never parse speculatively, and never lose precision. The solver tells you what types are possible, you check which ones the raw input satisfies, and disambiguation happens lazily.
Performance
- O(1) field lookup: Inverted index maps field names to bitmasks
- O(configs/64) narrowing: Bitwise AND to filter candidates
- Zero allocation during solving: Schema is built once, solving just manipulates bitmasks
- Early termination: Stops as soon as one candidate remains
Typical disambiguation: ~50ns for 4 configurations, <1µs for 64+ configurations.
Why This Exists
Serde's #[serde(flatten)] and #[serde(untagged)] have fundamental limitations
because they buffer values into an intermediate Content enum, then re-deserialize.
This loses type information and breaks many use cases.
Facet takes a different approach: determine the type first, then deserialize directly. No buffering, no loss of fidelity.
Serde Issues This Resolves
| Issue | Problem | Facet's Solution |
|---|---|---|
| serde#2186 | Flatten buffers into Content, losing type distinctions (e.g., 1 vs "1") |
Scan keys only, deserialize values directly into the resolved type |
| serde#1600 | flatten + deny_unknown_fields doesn't work |
Schema knows all valid fields per configuration |
| serde#1626 | flatten + default on enums |
Solver tracks required vs optional per-field |
| serde#1560 | Empty variant ambiguity with "first match wins" | Explicit configuration enumeration, no guessing |
| serde_json#721 | arbitrary_precision + flatten loses precision |
No buffering through serde_json::Value |
| serde_json#1155 | u128 in flattened struct fails |
Direct deserialization, no Value intermediary |
Sponsors
Thanks to all individual sponsors:
...along with corporate sponsors:
...without whom this work could not exist.
Special thanks
The facet logo was drawn by Misiasart.
License
Licensed under either of:
- Apache License, Version 2.0 (LICENSE-APACHE or http://www.apache.org/licenses/LICENSE-2.0)
- MIT license (LICENSE-MIT or http://opensource.org/licenses/MIT)
at your option.