The Extraction Schema Reference
The jsonSchema string sent to POST /lmkit/v1/extract-structured-data is the complete
specification of what comes back: its structure, its types, and its constraints. The engine
does not merely show the schema to the model and hope: structure and hard constraints are
compiled into a generation grammar that makes invalid output unrepresentable, steering hints
shape token choices during generation, and normalization rules clean values afterward. This
page is the exhaustive reference for that dialect: every keyword, every type, every
enforcement guarantee. For the surrounding workflow (confidence, review routing, batch
pipelines), see Structured Extraction.
1The shape of a schema#
The root is a JSON object. It may carry:
| Root keyword | Effect |
|---|---|
properties |
The fields to extract, one entry per output key. The root may also BE the field map directly, with no properties wrapper. |
required |
Array of field names that must hold a value. Anything not listed is optional and may come back null. |
title, description |
Shown to the model as context for the whole extraction. Use description for what the document IS ("a supplier invoice"); it is the schema-level counterpart of the request's guidance. |
allOf |
Dependent-value declarations (section 7). |
$schema |
Accepted and ignored, so schemas copied from other tooling paste cleanly. |
The extracted JSON contains every declared field, always, in declaration order: a field
that was not found is present with null (or an empty date), never silently missing. Output
keys are your property names exactly as written.
2Declaring a field#
Three equivalent forms, from terse to fully specified:
{
"invoice_number": "string",
"tags": ["string"],
"total": {
"type": "number",
"description": "Grand total including tax."
}
}
- Shorthand: the value is a type name (
"field": "string"). Array type names work here too ("field": "stringArray"). - Array shorthand: a one-element array of a type name (
["string"]) or of an object shape ([ { ...properties... } ]for an array of objects). - Object form: a
typeplus any of the keywords in section 4. This is the only form that can carry constraints.
3Types#
The type keyword accepts the engine's own names plus the standard JSON Schema aliases:
type |
Output | Notes |
|---|---|---|
string |
JSON string | The default workhorse. |
char |
JSON string | Single character. |
integer |
JSON integer | Signed. int width applies when values are parsed. |
uint, short, ushort, long, ulong |
JSON integer | Same integer grammar; the name states the width contract for your parser. |
number |
JSON number | Alias of double. Decimals and exponents allowed. |
double, float |
JSON number | |
boolean |
true / false |
Alias of bool. |
date |
"YYYY-MM-DD" |
The grammar enforces the ISO calendar-date shape, whatever format the document used. An absent optional date is an empty string or null. |
object |
Nested object | Declare children under properties (section 6). |
array |
JSON array | Declare entries under items (section 5). |
Arrays exist for every scalar (stringArray, integerArray, doubleArray, boolArray,
dateArray, ...), reachable through {"type": "array", "items": {"type": ...}}, the
["string"] shorthand, the direct name, or "isArray": true beside a scalar type.
4Field keywords#
The complete set. Enforcement says which layer guarantees the keyword: grammar means the constraint is compiled into generation and cannot be violated in the output; steering means it is shown to the model and guides token sampling; post means it is applied to the value after generation.
| Keyword | Applies to | Effect | Enforcement |
|---|---|---|---|
description |
any | Tells the model what the field means. The highest-leverage line in the schema: one precise sentence beats a paragraph. | steering |
enum |
string, string arrays | Closed set of allowed values, emitted exactly as declared. Include "" in the set when "none of these" must be expressible on a required field. allowedValues is accepted as an alias. |
grammar |
format |
string | date-time, time, email, hostname, ipv4, ipv6, uri steer generation toward the format. "format": "date" (or any type name) instead CONVERTS the field to that type, so {"type": "string", "format": "date"} is the classic way to declare a date. |
steering (conversion: grammar) |
pattern |
string, string arrays | Shape the whole value must match. Takes a regular expression (literals, character classes, \d \w \s, quantifiers * + ? {m,n}) or a compact descriptor: N digit, A uppercase letter, a lowercase letter, X uppercase alphanumeric, so an IBAN prefix is 2A2N. Convertible patterns compile into the grammar; a regex using features beyond that subset (groups, alternation, anchors) demotes to steering. Invalid syntax is rejected at request time. |
grammar (fallback: steering) |
maxLength |
string | Hard cap on the value's character count. Budgets above 1999 are enforced at 1999, the largest bound the generation grammar compiles. On numeric fields and string arrays it demotes to steering. | grammar |
maxItems |
any array | Hard cap on entry count: the grammar closes the array at the limit, so generation terminates there naturally. maxEntries is accepted as an alias. Prefer it for correctness guarantees: large bounds grow the grammar and cost generation speed. |
grammar |
caseMode |
string, string arrays | "UpperCase" or "LowerCase": the character set itself excludes the other case during generation. |
grammar |
disableSpacingCharacters |
string, string arrays | true removes the space character from the field's alphabet: identifiers come out unspaced ("AB 12 34" can only generate as "AB1234"). |
grammar |
trimStart |
string, string arrays | String or array of candidate prefixes: the longest one matching the extracted value is removed, once, so ["INV", "INV-"] turns "INV-2024-001" into "2024-001". Declaration order does not matter. |
post |
isArray |
any scalar type |
true turns the declared type into its array form. |
grammar |
Precedence rules the engine applies, in order:
- A
formatnaming a type converts the field first; keywords incompatible with the new type are then ignored (anenumbeside"format": "date"is dropped). enumwins over everything else on the field: values generate exactly as declared, sopattern,maxLength,caseMode, anddisableSpacingCharactersare not applied to them. Avoid combiningtrimStartwithenum: trimming runs after and can mangle a declared value.- Keywords on a type they do not apply to are ignored silently (an
enumon an integer does nothing: encode the constraint indescription, or extract as string and parse). - Unknown keywords are ignored, so schemas carrying annotations from other tools still parse.
5Arrays#
{
"tags": {
"type": "array",
"items": { "type": "string" },
"maxItems": 5,
"description": "Up to five thematic tags."
},
"line_items": {
"type": "array",
"items": {
"type": "object",
"properties": {
"description": { "type": "string" },
"quantity": { "type": "number" },
"unit_price": { "type": "number" }
},
"required": [ "description", "quantity", "unit_price" ]
}
}
}
- Scalar arrays hold one primitive type. Entry-level constraints ride on the array field
itself: an
enumconstrains every entry to the set, apatternshapes every entry,caseModeanddisableSpacingCharactersapply per entry. - Arrays of objects declare a row shape under
items; each row is a full nested scope with its ownrequiredand its own dependent values. maxItemsbounds either kind at the grammar level.- An empty array is a valid result for an optional array field; a required one must produce
at least the empty
[]rather thannull.
6Nested objects#
An object field declares children under properties; nesting recurses to any depth, and
arrays of objects can contain objects containing arrays. Two scoping rules:
requiredis per level: the root's array names root fields, each object's array names its own children. Marking a child required only matters once its parent produced a value.- Every level's fields appear in the output in declaration order, exactly like the root.
{
"vendor": {
"type": "object",
"description": "Seller information.",
"properties": {
"name": { "type": "string" },
"email": { "type": "string", "format": "email" },
"address": {
"type": "object",
"properties": {
"city": { "type": "string" },
"country": { "type": "string", "caseMode": "UpperCase", "maxLength": 2 }
}
}
},
"required": [ "name" ]
}
}
7Dependent values: two-level taxonomies#
When the valid values of one field depend on another field's answer (category and
subcategory, department and team), flat enums allow nonsense combinations. Declare the
dependency with standard allOf / if / then conditionals at the level where both fields
live:
{
"properties": {
"category": { "type": "string" },
"sub_category": { "type": "string" }
},
"required": [ "category", "sub_category" ],
"allOf": [
{
"if": { "properties": { "category": { "const": "Baggage" } } },
"then": { "properties": { "sub_category": { "enum": [ "Delay", "Damage", "Loss" ] } } }
},
{
"if": { "properties": { "category": { "const": "Flight Disruption" } } },
"then": { "properties": { "sub_category": { "enum": [ "Cancellation", "Delay", "Downgrade" ] } } }
}
]
}
The engine compiles the pair into one branching grammar unit: the model first commits to a
category, and from that point only that branch's sub_category values are generatable. An
undeclared combination is not merely discouraged: it cannot be produced.
The contract, precisely:
- The discriminator (
category) must be declared immediately before the dependent field: the two generate as one unit. - Each
ifnames exactly one property with aconststring; eachthennames exactly one property with a non-emptyenum. Entries of any other shape are ignored, so unrelated conditionals in a pasted schema do not break parsing. - One dependent field follows one discriminator; declaring conditionals on two different discriminators for the same field is rejected.
- The discriminator's own allowed values become the declared
constkeys automatically; an explicitenumon it must match those keys exactly. - An optional discriminator that generates
nullforces the dependent field tonull: a subcategory without its category names no combination at all. - The declarations work at any level, including inside array-of-object rows, where each row branches independently.
8Field names carry meaning#
The engine reads your property names semantically. A field named email, iban, siret,
phone, vat_number, or zip (among several hundred recognized names and their French,
German, Spanish, Italian, and Portuguese equivalents) is assigned an entity kind, which
activates a dedicated validator on the extracted value. That is what feeds the per-field
validation status in the response: valid, repaired (auto-corrected, with the
original_value kept), invalid, or not_applicable (no validator for this field).
Recognized families include, by example:
| Family | Names that trigger it |
|---|---|
| Contact | email, phone, mobile, fax, website, url |
| Identity | first_name, last_name, full_name, company, job_title |
| Address | street_address, city, postal_code, zip, state, country, country_code |
| Financial | iban, swift, bic, vat, tax_id, amount, total, currency, account_number |
| Identifiers | passport_number, national_id, license_plate, serial_number, invoice_number, uuid, isbn |
| Temporal | date, birth_date, datetime, timestamp, time, duration, age |
| Network | ip_address, ipv6, mac_address, hostname |
| Measurement | quantity, weight, percentage, unit, latitude, longitude |
Detection is forgiving about separators and casing (customerEmail, customer_email, and
Customer Email all read as an email address) and checks type compatibility: amount on a
number activates the currency validator, but amount on a boolean activates nothing. The
practical rule: name fields what they are, and validation, repair, and review routing
come free. A generic name (field_3, value) extracts fine but validates as
not_applicable.
9Everything in one schema#
{
"title": "Claim intake",
"description": "A customer claim letter, possibly scanned.",
"type": "object",
"properties": {
"claim_reference": {
"type": "string",
"description": "The claim file reference.",
"trimStart": [ "REF:", "Ref." ],
"disableSpacingCharacters": true,
"caseMode": "UpperCase",
"pattern": "3A4N"
},
"claim_date": { "type": "string", "format": "date" },
"claimant": {
"type": "object",
"properties": {
"full_name": { "type": "string" },
"email": { "type": "string", "format": "email" },
"iban": { "type": "string" }
},
"required": [ "full_name" ]
},
"category": { "type": "string" },
"sub_category": { "type": "string" },
"claimed_amount": { "type": "number", "description": "Total amount claimed, taxes included." },
"attachments_mentioned": {
"type": "array",
"items": { "type": "string" },
"maxItems": 10
},
"is_repeat_claim": { "type": "boolean" }
},
"required": [ "claim_reference", "claim_date", "category", "sub_category" ],
"allOf": [
{
"if": { "properties": { "category": { "const": "Baggage" } } },
"then": { "properties": { "sub_category": { "enum": [ "Delay", "Damage", "Loss" ] } } }
},
{
"if": { "properties": { "category": { "const": "Booking" } } },
"then": { "properties": { "sub_category": { "enum": [ "Refund", "Change", "No-show" ] } } }
}
]
}
Every mechanism above is at work here: grammar-enforced reference shape, a normalized date,
entity validation on full_name, email, iban, and claimed_amount, a branching taxonomy
that cannot produce an undeclared pair, a bounded array, and a boolean that can only be
true, false, or null.
10The request around the schema#
The schema rides in a request whose other fields are quickly told: input carries the
document as plain text, a Base64-encoded file, or an uploaded file's identifier
(input_format says which), or as [image, caption] pairs for multimodal extraction from
photos and scans; enable_ocr (default true) lets pages without a text layer be read by the
configured OCR engine; guidance adds free-text interpretation rules that apply across the
whole document; model overrides the default; include_elements returns the per-field
detail block. Accepted file formats are the document pipeline's own, listed in
Document Processing.
Three behaviors worth knowing when designing schemas:
- Doubt resolves to null. The endpoint extracts conservatively: when the document does
not support a value, the field comes back
nullrather than guessed. A missing REQUIRED field additionally raises the review flag, so "required but absent" becomes a routed exception instead of an invention. - Long documents are windowed. Content beyond the model's context is read in overlapping windows and merged, so the schema's size budget is about output, not input: a 200-page contract with a 12-field schema is fine.
- Slow extractions become jobs. Past the configured timeout the endpoint returns
202 Acceptedwith ajob_idto poll, the jobs contract every task endpoint shares.
One distinction to keep sharp: the chat API's response_format.json_schema shapes a
CONVERSATION's reply and follows the OpenAI structured-outputs dialect. This page's dialect
belongs to the extraction endpoint, where documents, OCR, confidence, validation, and source
coordinates live. When the task is "read this document into this shape", use extraction.