Validation and repair
What happens between the model's answer and your typed output, and how to add your own rules.
The pipeline
Every answer goes through the same steps; each one has its own error.
- Finish reason: a refusal becomes
Error::Refusal, a cut-off answerError::Truncated. Neither is repaired. - JSON extraction: code fences, text around the JSON and trailing commas are tolerated. An
unclosed value is reported, never completed. Failure:
Error::InvalidJson. - Schema validation: against the schema generated from the output type. Every violation is
collected with its path, e.g.
$.items[2].quantity: expected a value <= 255, got 300. Failure:Error::SchemaViolation. - Deserialization into the output type. Failure:
Error::Deserialize. - Your rules from
#[lm(validate = …)]. Failure:Error::Validation.
Repair
When a step from 2 to 5 fails, the program sends the answer back with a description of what is
wrong — all violations at once — and asks for a corrected answer. Program::max_repairs sets how
often (default 2). With 0, the first error is returned as is. When all attempts fail, the result is
Error::RetryLimitExceeded { attempts } with every attempt’s error, the last one last.
Your own rules
Rules the schema cannot express go into a function. Its messages are sent to the model, so phrase them as instructions.
/// Extract the invoice data from the text. Amounts are in cents.
#[derive(TypedLm, Serialize)]
#[lm(output = Invoice, validate = items_add_up)]
struct ExtractInvoice {
text: String,
}
fn items_add_up(invoice: &Invoice) -> Result<(), Vec<String>> {
let items: u64 = invoice.items.iter().map(|i| u64::from(i.quantity) * i.unit_price_cents).sum();
if items != invoice.net_cents {
return Err(vec![format!(
"the line items add up to {items} cents but net_cents is {}; check quantities and unit prices",
invoice.net_cents
)]);
}
Ok(())
}
Numeric ranges and string lengths are schema keywords; express them with schemars attributes
(#[schemars(range(min = 0, max = 100))]) and the validator checks them.
Errors and personal data
Variants such as InvalidJson and SchemaViolation keep the model’s raw answer in a field.
Display never prints model output — not even values quoted in a violation — so logging an error
does not leak it. Error::kind() returns a stable name for metrics, e.g. schema_violation.
Without a program
parse_output::<S>(&response, &output_schema::<S>()) runs the pipeline on a single Response,
for example in tests or in your own provider loop.