Four coercion levels, one API
| Level | What it does | Use when |
|---|---|---|
| Exact | No type conversion at all | Production validation |
| SafeWidening | int→float, bool→int (lossless only) | Conservative pipelines |
| StringCoercion | "42"→42, "true"→true, null sentinels | CSV/config/env data |
| BestEffort | Everything above + JSON stringify, array unwrap | Exploratory/prototyping |
Every coercion produces a diagnostic — what happened, the risk level, and a suggestion for tightening.
Lenient, Absorbing, Strict
A mode sets four switches at once. It's a type parameter, so the choice is visible in the type and checked at compile time — Strict carries a residual of Infallible, making a strict result a compile-time proof that nothing was coerced or left over.
| Lenient | Absorbing | Strict | |
|---|---|---|---|
| Unknown fields | dropped | preserved in overflow | error |
| Coercion level | BestEffort | SafeWidening | Exact |
| Missing fields | defaulted | error | error |
| On error | collect all | collect all | fail fast |
| Residual type | () | Overflow (HashMap) | Infallible |
When the mode comes from config rather than code, a DynamicMode enum parses from the strings lenient, absorbing, and strict.
Every adjustment, recorded
Each coercion, default, drop, and preservation becomes a Diagnostic { path, kind, risk, suggestion }. Risk levels are ordered Info < Warning < Risky, so gating a production path on “no Risky diagnostics” is a one-liner.
Diagnostics route through a DiagnosticSink: collect into a Vec, print to stderr, forward only at or above a risk threshold, or drop them.
20 data types from raw strings
guess_type() identifies types with confidence scores. Multiple types can match — the full ranked list is returned.
| Category | Types detected |
|---|---|
| Primitives | Integer, Float, Boolean, NullSentinel |
| Dates | ISO 8601, US/EU dates, Unix timestamps, HL7, compact, week dates |
| Finance | Currency (19 symbols), Credit Card (Luhn), IBAN (MOD-97) |
| Identity | UUID, Email, Phone, SSN, EIN, NHS, NPI, EU VAT, ISBN |
| Structure | JSON, URL, IP Address, Unit Value |
Built-in parsers for real-world data
Dates
18 format detection and conversion: ISO 8601, US/EU slash dates, dot-separated European, dash-separated, HL7 v2, Unix seconds/millis, abbreviated months, GEDCOM, year-only, ISO week dates.
Currencies
19 symbols (including A$, C$, HK$, €, £, ¥), 29 currency codes, locale detection (US dot-decimal vs European comma-decimal), accounting negative format.
Medical
36 analytes, 44 unit conversions (glucose, cholesterol, HbA1c, creatinine, etc.), reference range classification, clinical calculators (eGFR, BMI, corrected calcium, anion gap, creatinine clearance), FHIR observation parsing, HL7 datetime, pharma abbreviations.
Identifiers
12 types with format validation and checksums: IBAN (MOD-97), Credit Card (Luhn + BIN detection for Visa/MC/Amex/Discover/JCB/Diners/Maestro), ISBN-10, ISBN-13, US SSN, US EIN, US NPI (Luhn), UK NHS (MOD-11), EU VAT (country-specific), UUID, Email, Phone.
Units
Weight, length, volume, temperature, data, time. Qualified weight (gross/net/tare), pack notation (6x500ml, case of 12), UN/ECE code recognition, unit conversion.
Coordinates
Decimal degrees, DMS (degrees/minutes/seconds), DDM (degrees decimal minutes), ISO 6709. Latitude/longitude validation, order detection.
Anthropic, OpenAI, Ollama — one struct
Parse responses from any provider into a unified NormalizedResponse:
// Same struct regardless of provider
let response = parse_openai_response(&raw_body)?; // or anthropic / ollama
let text = response.text();
let tools = response.tool_uses();
let usage = response.usage; // input_tokens, output_tokens,
// cache_read_tokens, cache_creation_tokens
// Model wrapped its JSON in prose or a code fence? Pull it out first (0.4.1):
let data = FlexValue::from_llm_response(model_text)?;
Round-trip: parse -> emit -> re-parse preserves all data. from_llm_response extracts a JSON payload from markdown-fenced or prose-wrapped model output before parsing, while from_json stays strict. Token usage is normalized across providers, including cache_read_tokens from OpenAI and Anthropic prompt caching.
Push bytes, get events
The streaming parser is push-style: feed it chunks as they arrive, and it returns parse events incrementally. SSE and newline-delimited JSON both ride on top of it.
use laminate::{FlexStream, StreamConfig};
let mut stream = FlexStream::new(StreamConfig::default());
for chunk in byte_chunks {
for event in stream.feed(chunk) { // feed(&[u8]) -> Vec<StreamEvent>
handle(event);
}
}
for event in stream.finish() { // flush anything still buffered
handle(event);
}
Struct shaping with compile-time safety
#[derive(Laminate)]
struct Config {
#[laminate(coerce)] // auto-convert string -> number
port: u16,
#[laminate(coerce, default)] // missing -> false, "yes" -> true
debug: bool,
#[laminate(rename = "apiKey")]
api_key: String,
#[laminate(overflow)] // unknown fields -> HashMap
extra: HashMap<String, Value>,
}
Three shaping modes
shape_lenient()
Coerce and continue, diagnostics available
shape_absorbing()
Capture unknown fields in overflow
shape_strict()
Reject any coercion or unknown fields
Seven field attributes in all: coerce, default, overflow, rename, parse_json_string, flatten, and skip. Enums too: the derive also works on string-valued enums, where a separate #[laminate(unknown)] newtype variant captures an unrecognized value with a diagnostic instead of failing the parse.
Beyond the core library
laminate-sql
Shape rows straight from a SQL source via sqlx. Enable a backend feature: sqlite, postgres, or mysql.
laminate-cli
Profile and validate data from the command line: infer a schema, audit data against it, and inspect values for detected types.
Infer structure from data, then audit against it
- Per-field type detection (dominant type, mixed-type flag)
- Fill rate and null tracking
- Required field inference
- External constraints (override inferred types/nullability)
- Violation reporting (TypeMismatch, UnexpectedNull, MissingRequired, UnknownField, ConstraintViolation)
Context-aware coercion defaults
Tell laminate where the data came from, and it adjusts coercion defaults:
| Hint | Default coercion | Why |
|---|---|---|
| SourceHint::Csv | StringCoercion | CSV columns are always strings |
| SourceHint::Json | Exact | JSON preserves types |
| SourceHint::Env | StringCoercion | Env vars are always strings |
laminate 0.4.1 · crates.io · docs.rs · GitHub · v0.4.1 release notes