In 2017, the maintainer of serde closed a feature request with a wish. He said he'd love to see someone explore a separate library "specifically geared toward fault-tolerant partially successful deserialization." For years, nobody built it. I kept running into the exact gap he was describing, so eventually I did. It's called Laminate, and the whole idea fits in one sentence: Be liberal in what you accept, and shape it into your types one layer at a time.
The problem serde leaves on the table, on purpose
serde is one of the best libraries in any language, and its strictness is a feature, not a flaw. When you control both ends of a pipe, you want deserialization to be exact and to fail loudly the moment reality doesn't match your struct. That's correct.
But a lot of the data we actually deal with isn't data we control. An API returns an ID as a number on Monday and a string on Friday. A CSV column is always a string, even when it's obviously a date. A config file mixes types freely. An LLM hands you JSON where the tool arguments are themselves a stringified blob of more JSON. serde meets the first of these surprises and stops, which leaves you two bad options: Pre-clean every input by hand, or write a thicket of custom deserializers for each shape of mess.
Laminate is the third option. It sits in the space serde's maintainer pointed at, between
serde_json::Value and your typed structs, and it's built on serde rather than against it. It
accepts a Value, and it hands you back serde types.
The idea: shape data in layers, and choose how forgiving each one is
The core type is FlexValue, a wrapper over serde_json::Value that you can navigate by path
and coerce toward the types you actually want:
use laminate::FlexValue;
let data = FlexValue::from_json(r#"{"port": "8080", "debug": "true"}"#)?;
let port: u16 = data.extract("port")?; // "8080" becomes 8080
let debug: bool = data.extract("debug")?; // "true" becomes true
The "layer by layer" part is literal. You pick how forgiving the shaping is, with four coercion levels: Exact does no conversion at all, SafeWidening allows only lossless promotions like int to float, StringCoercion turns "42" into 42 and "true" into true, and BestEffort goes furthest. And every coercion produces a diagnostic that tells you what happened and how risky it was, so you can be forgiving without being blind. You can run a permissive pipeline in development and tighten it toward Exact in production, changing one parameter, not rewriting your types.
That's the name, by the way. You laminate data the way you laminate a material: Bonding thin layers into something with structure and strength, each layer adding a little more shape.
A short history
Laminate grew out of my own work, where I kept needing to consume data that wasn't well behaved, LLM API responses most of all. The pattern of "accept the mess, shape it, keep an audit trail" turned out to generalize well beyond that first use, so it became a small family of crates: The core library, a derive macro, SQL connectors, and a CLI. It's on crates.io now under MIT or Apache-2.0, and it's tested hard, with a corpus of tens of thousands of cases and millions of fuzz runs behind it. The point of that testing isn't to brag about a number. It's that a library whose whole job is handling weird input has to have actually seen a lot of weird input.
What Laminate is actually for
The reason to reach for Laminate is always a specific kind of mess. Here are the ones it was built around.
Consuming LLM and AI responses. This is the use case that started it. Anthropic stringifies tool arguments, OpenAI streams a response in fragments across dozens of server-sent events, and every provider's schema drifts without warning. Laminate's provider adapters parse Anthropic, OpenAI, and Ollama into one normalized shape, and the streaming parser assembles the fragments for you:
let response = parse_anthropic_response(&raw_body)?;
let text = response.text();
for tool_call in response.tool_uses() {
let query: String = tool_call.input().extract("query")?;
}
CSV, config, and environment data. Here, everything is a string, and every pipeline ends up reinventing string-to-number conversion with its own subtly different edge cases. You tell Laminate where the data came from, and it sets sensible coercion defaults:
let row = FlexValue::from_json(csv_row)?.with_source_hint(SourceHint::Csv);
let price: f64 = row.extract("price")?; // "29.99" becomes 29.99
let count: i64 = row.extract("quantity")?; // "42" becomes 42
Third-party REST APIs. When the schema can change under you, the derive macro lets a struct bend without breaking: Coerce the fields that drift, default the ones that go missing, and capture unknown fields instead of dropping them.
#[derive(Laminate)]
struct ApiUser {
#[laminate(coerce)] id: i64, // "123" or 123
name: String,
#[laminate(default)] email: Option<String>, // missing becomes None
#[laminate(overflow)] extra: HashMap<String, Value>, // unknown fields kept
}
Profiling and auditing data you don't trust yet. Before you can rely on a dataset, you need to know what's actually in it. Laminate can infer a schema from a sample, then audit new data against it and report exactly where it violates: A field that changed type, a required value that went missing, a column that's secretly mixed.
Detecting what a string really is. Sometimes you just have a column of strings and need to
know which are dates, which are currencies, which are UUIDs or credit cards. guess_type
identifies more than sixteen types, with a confidence score and the full ranked list, so a
string like "$1,234.56" comes back as a currency and "4111111111111111" passes the Luhn check
as a card number, not just an integer.
On top of that sit domain packs for the data that always seems to need special handling: Dates in fifteen-plus formats, currencies with locale and accounting-negative awareness, medical lab values with unit conversion, identifiers with real checksums, and units. Each pack is the result of one of those "why is this so hard every single time" afternoons, packaged so it's hard exactly once.
When not to reach for it
Laminate earns its place when the data is messy and you don't own it. If you control both ends of the wire, use serde directly and enjoy the strictness. If you need maximum throughput with zero conversion overhead, that's serde again. If all you want is to merge configuration from a few sources, figment is more focused. Laminate is for the moment the input stops cooperating.
Try it
The library serde's maintainer wished for in 2017 is a
cargo add laminate away. It's
open source, it builds on serde instead of
replacing it, and it's happiest doing the unglamorous work of turning the world's messy data
into types you can actually rely on. If you've been hand-cleaning inputs or writing one more
custom deserializer, I'd genuinely like to know whether it saves you the trouble.