Laminate › Use Cases

Real problems Laminate solves

From config files and CSVs to APIs, LLM output, and domain data — see Laminate in action

AI / LLM

AI/LLM Response Handling

The Problem

LLM APIs return messy, inconsistent JSON. Anthropic stringifies tool arguments. OpenAI streams fragments across dozens of SSE events. Schema changes arrive without warning. And serde crashes on the first surprise.

The Solution

ai_response.rs
use laminate::provider::anthropic::parse_anthropic_response;

let response = parse_anthropic_response(&raw_body)?;  // or openai / ollama
let text = response.text();
for tool_call in response.tool_uses() {
    let (id, name, input) = tool_call.as_tool_use().unwrap();
    let query: String = input.extract("query")?;
}

// Token accounting, normalized across providers (0.4.1)
let cached = response.usage.cache_read_tokens;  // OpenAI/Anthropic prompt caching

When the model wraps its JSON in a markdown fence or surrounding prose, from_llm_response pulls the payload out before parsing (new in 0.4.1). The same adapters cover Anthropic, OpenAI, and Ollama:

from_llm_response.rs
use laminate::FlexValue;

// Extracts JSON from fenced or prose-wrapped model output, then parses
let data = FlexValue::from_llm_response(model_text)?;
let answer: String = data.extract("answer")?;

Provider Adapters documentation →

DATA INGESTION

CSV/Config File Parsing

The Problem

CSV columns are always strings. Environment variables are always strings. Config files mix types freely. Every pipeline builds its own string-to-number conversion, and every one has different edge cases.

The Solution

csv_ingest.rs
use laminate::FlexValue;
use laminate::value::SourceHint;

// Tell laminate the data came from CSV — enables string coercion by default
let row = FlexValue::from_json(csv_row_json)?
    .with_source_hint(SourceHint::Csv);

let price: f64 = row.extract("price")?;       // "29.99" -> 29.99
let active: bool = row.extract("active")?;    // "true" -> true
let count: i64 = row.extract("quantity")?;    // "42" -> 42

Source Hints documentation →

API INTEGRATION

REST API Consumption

The Problem

External APIs change schemas without notice. Optional fields appear and disappear. Types drift — an ID that was always a number suddenly arrives as a string. Your pipeline needs to handle what it gets, not what the docs promise.

The Solution

api_user.rs
use laminate::Laminate;

#[derive(Laminate, Debug)]
struct ApiUser {
    #[laminate(coerce)]
    id: i64,                          // handles "123" or 123
    name: String,
    #[laminate(default)]
    email: Option<String>,            // missing -> None
    #[laminate(overflow)]
    extra: HashMap<String, Value>,    // unknown fields captured, not dropped
}

let user = ApiUser::shape_lenient(&api_response)?;
// user.value has your struct; user.diagnostics tells you what was coerced

Derive Macro documentation →

DATA QUALITY

Data Quality Auditing

The Problem

You need to know what's IN your data before you can trust it. How many nulls? What types does each column actually contain? Does the real data match the documented schema?

The Solution

schema_audit.rs
use laminate::schema::InferredSchema;

let schema = InferredSchema::from_values(&sample_rows);
println!("{}", schema.summary());
// Columns: id(Integer, 100% fill), name(String, 98% fill),
//          score(Float/String mixed, 95% fill)

let report = schema.audit(&new_data);
for violation in &report.violations {
    // ViolationKind: TypeMismatch, UnexpectedNull, MissingRequired,
    //                UnknownField, ConstraintViolation
    println!("{violation:?}");
}

Schema Inference documentation →

HEALTHCARE

Healthcare / Lab Data

The Problem

Medical lab values use different units across countries (mg/dL vs mmol/L). Date formats vary wildly (ISO, HL7, FHIR). Identifier validation (NHS numbers, NPIs) is scattered across niche libraries.

The Solution

medical.rs
use laminate::packs::medical::{convert_lab_value, classify_lab_value};

let glucose_si = convert_lab_value(126.0, "glucose", "mg/dL", "mmol/L");
// Some(6.99) — US conventional to SI units

let classification = classify_lab_value(126.0, "glucose", "mg/dL");
// Some(High) — above normal fasting range

Medical is one of six domain packs. The same packs module also covers currency, units, identifiers, geographic coordinates, and time — each one the messy-parsing afternoon you'd rather not repeat.

Medical Pack documentation →

DATA PROFILING

Type Detection & Data Profiling

The Problem

You have a column of strings. Some are dates, some are numbers, some are UUIDs, some are credit card numbers. You need to know what they are before you can process them.

The Solution

detect.rs
use laminate::detect::guess_type;

let guesses = guess_type("4111111111111111");
// [CreditCard(0.90), Integer(0.50)] — credit card wins

let guesses = guess_type("2026-04-06T14:30:00Z");
// [Date(Iso8601, 0.85)] — ISO 8601 datetime

let guesses = guess_type("$1,234.56");
// [Currency(0.90)] — US dollar format

Type Detection documentation →

STRICTNESS

Lenient in development, strict in production

The Problem

In development you want to see exactly what a provider is sending and keep going. In production you want a surprise to stop the line, not slip through as a quiet coercion. Maintaining two struct definitions for that is a tax.

The Solution

modes.rs
use laminate::{Laminate, DynamicMode};

// One struct, three strictness levels — only the entry point changes
let dev     = Config::shape_lenient(&value)?;    // coerce, default, record everything
let staging = Config::shape_absorbing(&value)?;  // keep unknown fields in overflow
let prod    = Config::shape_strict(&value)?;     // refuse coercion; fail on a surprise

// Or choose the mode at runtime, from config
let mode: DynamicMode = "strict".parse()?;

Operational Modes documentation →

OBSERVABILITY

Know exactly what the parser changed

The Problem

When you shape data you don't control, a silent coercion is a future bug. You need a record of every adjustment — and a way to gate a production path on whether anything risky happened.

The Solution

diagnostics.rs
use laminate::RiskLevel;

let result = Order::shape_lenient(&payload)?;

// Every coercion, default, and dropped/preserved field is recorded,
// each with a path, a kind, and a risk level (Info < Warning < Risky)
for d in &result.diagnostics {
    println!("[{:?}] {}: {:?}", d.risk, d.path, d.kind);
}

// Gate a production path on "nothing risky happened"
let risky = result.diagnostics.iter().any(|d| d.risk >= RiskLevel::Risky);

Diagnostics documentation →

SOURCES & CLI

Audit a database or a pile of files, without writing a program

The Problem

Sometimes the data lives in a Postgres table or a directory of JSONL, and you just want to profile it or check it against a schema — today, from the terminal, not after building a tool first.

The Solution

shell
# Infer a schema from a JSONL file, then audit a table against it
laminate infer data/events.jsonl > schema.json
laminate audit --schema schema.json "postgres://localhost/app?table=events"
laminate inspect data/sample.json   # type + fill-rate profile of any payload

laminate-sql reads Postgres, SQLite, MySQL, and JSON/JSONL files; laminate-cli wraps infer / audit / inspect. Both are separate companion crates.

Companion crates →