Guide · Structured data extraction

Valid JSON is not valid data.

The tooling for making a model emit parseable output is now excellent — JSON modes, function calls, grammar constraints. Which is exactly why teams get burned: the output parses, lands, and is wrong in ways syntax can never catch. Structured extraction is a contract problem before it is a prompting problem. This guide is about the contract.

12 min read · Updated August 2026 · By the Tablize team

01 / DefinitionWhat structured data extraction is

Definition

Structured data extraction is converting unstructured or semi-structured input — pages, documents, free text — into values that conform to a declared schema: named fields, fixed types, closed enums, explicit nullability. The schema is not documentation of the output; it is the definition of the deliverable. Extraction without one produces text that merely looks like data.

The field used to mean wrestling HTML into rows with XPath. The LLM era inverted the difficulty: models read anything, so getting a structure out is trivial — getting the declared structure out, ten thousand times in a row, with drift caught and hallucination refused, is the whole discipline. Three ideas carry it: the contract, the mechanical judge, and batch-level acceptance.

02 / The contractThe schema is the contract — and it is settled before row one

Every downstream consumer — the filter, the join, the chart, the next model — binds to the schema, not to the prose that produced it. Which is why the two schema sins are so expensive:

  • Discovering the schema as you go. Batch 1 returns category: "OEM", batch 3 introduces "OEM/ODM", batch 7 has "manufacturer (OEM)". Each looked reasonable in the moment; the column is now three vocabularies wearing one name, and every consumer needs a translation layer nobody declared.
  • Leaving nullability implicit. If the schema doesn't say a field may be unknown, the model decides for itself — usually by helpfully inventing. The licence to answer null is part of the contract, not a prompt nicety.

A schema worth the name declares, per field: type, allowed values (a closed enum wherever the field is categorical), nullability with meaning (unclear distinct from absent), units where numeric, and an example of a refusal — what the model should emit when the evidence isn't there. Settle it with whoever runs the first batch; change it only by explicit version, re-running affected rows under the new contract. Silent schema evolution is how columns rot.

# a contract, not a wish — this is what every batch is judged against
{
  "category":   { "enum": ["manufacturer", "trader", "unclear"] },
  "moq":        { "type": "integer", "unit": "pieces", "null": "not stated" },
  "founded":    { "type": "year", "null": "not stated" },
  "evidence":   { "type": "string", "note": "quote from the row, not a paraphrase" }
}

03 / MechanicsGetting structure out of a model — what each method actually guarantees

MethodGuaranteesDoes not guarantee
Prompted JSONNothing formalParsing; models decorate with prose exactly when it hurts
JSON modeSyntactic validityField names, types, enum membership
Function / tool callingShape: fields and primitive typesSemantic validity — hallucinated values type-check fine
Grammar-constrained decodingEverything syntactic, to the tokenTruth. A grammar cannot know the page didn't state a founding year

Read the right-hand column top to bottom: it never empties. Constraint tooling moves failure up the stack — from "didn't parse" to "parsed, conformed, and is wrong" — which is a real improvement and a more dangerous failure mode, because it is invisible. Use the strongest constraint your stack offers, then treat its output as claims to verify, never as facts that arrived pre-checked.

04 / The judgeDeclared checks: the mechanical judge on every submission

The verification layer is unglamorous on purpose: rules stated next to the schema, run against every submitted row, no discretion involved.

  1. Type and enum membership"likely manufacturer" is refused by the enum, not negotiated with.
  2. Ranges and formats — a founding year of 2031, an MOQ of −5, a date that isn't one: refused.
  3. Required-ness and null semantics — required fields present; nulls carry their declared meaning rather than standing in for "the model got tired".
  4. Evidence rules where they matter — for high-stakes columns, requiring a supporting quote from the source row turns "trust me" into "check me".

Mechanical checks catch malformed output. What they cannot catch is the well-formed lie — category: "manufacturer" for a company that is plainly a trader. For that you hide rows with pre-pinned answers inside normal batches and refuse batches that miss them. The two layers together — the judge and the spot-check — are the gate; the LLM extraction guide covers the gate's three modes in depth. In Tablize, declared checks run on every submission under every mode — the judge is not optional equipment.

05 / ScaleHow structure survives ten thousand rows

Structured extraction degrades at scale in a specific, measurable way: conformance per batch drifts. The instruments that catch it:

  • Batch size follows judgement, not capacity. A model that conforms perfectly on 25 rows and raggedly on 200 has told you its working set. Believe it.
  • Conformance rate per batch is a dashboard number. A dip from 100% to 92% is not noise to average away — it is the earliest signal that the prompt met input it doesn't understand, or the model changed underneath you.
  • Refuse and retry beats accept and patch. A batch that fails checks re-runs as attempt #2 against the same contract. Patching non-conformant output downstream builds a second, undocumented schema in the patch code.
  • The ledger remembers. Attempts, refusal reasons, models, batch ids — when drift is discovered late, the ledger answers "which rows, which era, which contract" without archaeology.

06 / FAQQuestions that come up

Doesn't JSON mode solve structured extraction?

It solves parsing. Field semantics, enum discipline, honest nulls and truthfulness remain yours. Valid JSON is not valid data — the checks that judge meaning still have to exist outside the model.

Who should own the schema?

Whoever runs the first batch — settled then, versioned thereafter. Silent mid-run evolution splits a column into multiple vocabularies that look like one. When change is needed, version it and re-run affected rows under the new contract.

Closed enums feel restrictive — why insist?

Because the enum is what downstream binds to. An open category field is an essay column wearing a data column's name: unfilterable, unjoinable, unaggregatable. If the world genuinely exceeds the enum, that discovery should arrive as refused rows demanding a schema version — visibly — not as vocabulary drift.

What belongs in declared checks vs spot-checks?

Anything a rule can state — types, enums, ranges, formats, required-ness — belongs to declared checks, which run on everything for free. Spot-checks against pinned answers cover the remainder: the well-formed value that is simply wrong. Rules first, sampling second; neither substitutes for the other.

How big should extraction batches be?

As big as the model's judgement stays flat — found empirically, and usually far below the context limit. Watch per-batch conformance and quality on pinned rows as you scale batch size; the knee in that curve is your number. Sizing to capacity instead of judgement is how quiet degradation starts.


Adjacent reading, same production-line lens:

Public beta · free · macOS, Linux and Windows

Declare the contract. The line enforces it.