An invoice parser returns subtotal: 4180, tax: 627 and total: 4807. Each value is valid JSON. Each value even matches text on the page. But the output cannot answer the question that matters next: which rows add up to that subtotal, and where did each row come from?

Flat fields are convenient at the API boundary because every consumer knows how to read a key and a value. They become dangerous when convenience is mistaken for meaning. A value without its relationship to the document, its peers and its evidence is not structured data. It is a confident-looking assertion.

Useful structured JSON preserves three things at once: the value, the relationship and the proof. Drop any one of them and a downstream system must reconstruct context from names, array positions or visual guesswork.

A table is not an array of unrelated objects

The usual invoice schema models line items as a list and totals as sibling fields:

{
  "lineItems": [
    { "description": "Inspection", "quantity": 2, "unitPrice": 900, "amount": 1800 },
    { "description": "Repair", "quantity": 7, "unitPrice": 340, "amount": 2380 }
  ],
  "subtotal": 4180,
  "tax": 627,
  "total": 4807
}

That shape is readable, but its relationships are only implied. The consumer assumes that every row belongs to the subtotal, that the subtotal is the tax base, and that the total is their sum. Those assumptions fail on real documents: a freight row may sit outside the taxable base, a discount may apply to one section, or the visible grand total may carry a prior balance.

The schema should make the document's own claims explicit. Give assertions stable identities, then describe the edges between them:

{
  "assertions": {
    "row_1.amount": { "value": 1800, "currency": "USD", "evidence": ["ev_17"] },
    "row_2.amount": { "value": 2380, "currency": "USD", "evidence": ["ev_24"] },
    "subtotal":     { "value": 4180, "currency": "USD", "evidence": ["ev_31"] },
    "tax":          { "value": 627,  "currency": "USD", "evidence": ["ev_34"] },
    "total":        { "value": 4807, "currency": "USD", "evidence": ["ev_38"] }
  },
  "relations": [
    { "type": "contributes_to", "from": "row_1.amount", "to": "subtotal" },
    { "type": "contributes_to", "from": "row_2.amount", "to": "subtotal" },
    { "type": "tax_applies_to", "from": "tax", "to": "subtotal" },
    { "type": "sums_to", "from": ["subtotal", "tax"], "to": "total" }
  ]
}

Now validation is not based on a naming convention. It follows declared edges. A consumer can calculate the subtotal from precisely the rows that contribute to it, display the tax base, and explain why the grand total is 4,807 instead of merely reporting that number.

Evidence is an address, not a screenshot

Every assertion should point back to the smallest source region that proves it. That evidence object belongs in one shared collection, referenced by ID:

{
  "evidence": {
    "ev_31": {
      "page": 2,
      "text": "Subtotal 4,180.00",
      "textSpan": { "start": 1842, "end": 1860 },
      "boundingBox": {
        "unit": "normalized",
        "polygon": [[0.68,0.72],[0.91,0.72],[0.91,0.75],[0.68,0.75]]
      }
    }
  }
}

The text span makes the proof searchable and diffable. The page and polygon make it visually reviewable. The assertion's reference to ev_31 makes the connection durable. Copying page coordinates into every field duplicates facts and invites drift; storing only a cropped image removes the text a machine needs.

This is where the boundary with bounding boxes as an API contract matters. That article answers how coordinates survive rotation, units and rendering. This one answers what the coordinate proves, which assertion owns it, and how that assertion relates to the rest of the result. Geometry locates evidence. Relationships give it meaning.

Stable IDs keep corrections attached

Array position is not identity. If a reviewer inserts a missed line at index 1, every later index changes. A correction stored against lineItems[4] may silently move to the wrong row.

Assign a stable row ID when the extraction result is created. It can be an opaque identifier; it does not need to encode the row number or description. Fields under that row inherit the row's identity, and relations point to the ID rather than the current position.

  • Order stays present — keep an explicit sequence property for display order.
  • Identity stays stable — reordering rows does not rewrite references.
  • Review stays attributable — an edit log can name the assertion that changed.
  • Diffs stay meaningful — an inserted row appears as an addition, not as twenty replacements.

The same rule applies above the row. A purchase order, shipment and invoice may be three documents in one process. Their extracted fields should relate through document IDs and business entities, not through whichever order the files arrived.

Validation belongs beside the relation

A subtotal check is often implemented as application code after extraction. That catches arithmetic, but the result disappears into a log. Put validation output into the contract so every consumer sees the same conclusion:

{
  "validations": [
    {
      "rule": "sum(contributes_to:subtotal) == subtotal",
      "status": "passed",
      "observed": 4180,
      "expected": 4180,
      "inputs": ["row_1.amount", "row_2.amount"],
      "target": "subtotal"
    }
  ]
}

A failed check should be equally explicit. It is not evidence that the parser is wrong; the source document itself may contain broken arithmetic. Preserve the printed subtotal, calculate the observed sum, report the mismatch and route it for review. Replacing the source value with the calculated one destroys the distinction between extraction and correction.

This also changes what confidence means. A model may be highly confident that the page says 4,810 while the rows sum to 4,180. Field confidence and relationship validity are separate signals. Combining them into one score hides the most actionable disagreement.

Provenance must survive human review

Review is not a reason to discard machine evidence. It is another event in the assertion's history. When a person changes subtotal.value, record the previous value, new value, actor, time and reason while retaining the evidence that produced the first assertion.

{
  "changes": [
    {
      "assertion": "subtotal",
      "operation": "replace",
      "from": 4810,
      "to": 4180,
      "actorType": "human",
      "reason": "printed subtotal disagrees with included rows",
      "at": "2026-09-02T18:42:11Z"
    }
  ]
}

The corrected value is what an approved export should expose by default. The original assertion remains available for audit, model evaluation and regression analysis. Without that history, a corrected dataset teaches only that the final value was right; it cannot teach why the machine was wrong or whether the same pattern returned.

Null, absent and not applicable are different

Structured output fails quietly when every missing value becomes null. A field may be absent because the document does not show it, unreadable because the region is damaged, withheld because policy redacted it, or not applicable to this document type. Those states drive different automation.

StateMeaningConsumer action
not_foundNo supporting evidence was foundReview if required
unreadableA likely region exists but cannot be read reliablyRequest a better source
redactedThe value is intentionally unavailableDo not retry extraction
not_applicableThe concept does not apply to this documentContinue normally

Keep the state beside the assertion and attach evidence when one exists. An unreadable field may still point to the damaged region. A redacted field may point to the redaction marker. Absence without an explanation forces every consumer to invent its own.

Version the graph, not just the envelope

A top-level schemaVersion is necessary, but it is not enough. Consumers need to know which changes preserve meaning. Adding a relation type is different from renaming an assertion ID; changing evidence units is different from adding an optional confidence component.

Define compatibility around operations consumers perform:

  • Resolve an assertion — IDs remain stable within a result and documented across revisions.
  • Traverse a relation — relation types have defined direction and cardinality.
  • Render evidence — page indexing, coordinate units and polygon order are versioned.
  • Replay a review — change events identify the schema version they edited.
  • Export an approved result — consumers can choose current values or full provenance.

Contract tests should use a document with repeated labels, multiple totals and at least one correction. Happy-path fixtures with one field named “Total” prove only that JSON serialization works.

The minimum useful contract

You do not need a universal knowledge graph to preserve meaning. Start with a small, explicit contract:

  1. Every extracted assertion has a stable ID, typed value and extraction state.
  2. Every material assertion references one or more evidence objects.
  3. Repeated structures use stable row or group IDs plus explicit order.
  4. Relations name their direction, members and business meaning.
  5. Validations publish inputs, target, observed result and status.
  6. Human changes append provenance instead of replacing history.
  7. Schema and evidence geometry are versioned as separate contracts.

That is enough for a reviewer to click from a total to the exact page region, for a workflow to decide which rows belong in a calculation, and for an auditor to reconstruct how the approved value emerged.

Structured JSON is not the number of nested braces in a response. It is whether the response preserves the document's claims after the page is gone. A field tells you what the system read. A relation tells you what it means. Evidence tells you why anyone should believe it.