A number needs a state
When an extractor returns a number, the application also needs to know what the source showed. Turning a blank or unreadable field into zero records a value the source did not supply. Turning every unresolved field into null loses another useful distinction: a visibly blank cell, a field not located in the pages inspected, and text the model could not read are different situations.
A fictional stock-count sheet
Imagine a made-up stock sheet for a small shop. The black tea row says “On hand: 0 packs.” The cocoa row has an empty count cell. A blurred cardamom row could read “3” or “8 packs.” No bay-leaf row appears on the one page submitted. Only the black tea row contains an explicit count of zero.
Keep the source states separate
- Found: the source explicitly says 0 packs. Store status=found, count=0, unit=packs, and the page and text span that support it.
- Blank: the cocoa row is present but its count cell is empty. Store status=blank and count=null, with a locator for that cell.
- Not located: no bay-leaf row was found on the supplied page. Store status=not_located, count=null, and scope=page 1. Do not infer that the item is absent from the full inventory.
- Unreadable: a value is visible but cannot be read reliably. Store status=unreadable, count=null, and a source locator.
- Ambiguous: the cardamom mark has two plausible readings. Store status=ambiguous, count=null, the candidates 3 and 8, and the source locator.
Null is a representation, not an explanation
In JSON, an omitted property and a property whose value is null are different. For this contract, the result object should always contain its count field; null means that this result is not asserting a count. The separate status explains why. A bare null cannot tell whether the source cell was blank, the field was not located in the inspected scope, or the text was unreadable. An omitted count key is a malformed result, not evidence about the source.
Keep evidence attached to the value
A found result can be small: status=found; count=0; unit=packs; source=page 1, “On hand: 0 packs.” Keep the original excerpt and page or region locator beside the normalized value. For an OCR pipeline, the application can check that the cited span exists in the preserved OCR text and that deterministic parsing of that span agrees with the proposed number. Those checks establish agreement with the OCR record, not the original image. OCR can misread the original image, so show the source region for review before a person confirms the draft.
Tests for the extraction boundary
- Read “0 packs” from a clear source span. Expect found with numeric value 0; never replace it with null or treat it as missing.
- Read a visibly empty count cell. Expect blank with a null value and its row or cell locator; do not create a zero count.
- Search a submitted page with no matching row. Expect not_located with the inspected scope; do not say the item is absent from pages that were not supplied.
- Read a field whose marks are visible but cannot be interpreted. Expect unreadable with count=null and its source locator; do not invent a candidate number.
- Read a blurred mark with plausible values 3 and 8. Expect ambiguous with both candidates and no normalized count.
- Omit the count property, or pair status=found with a null value. Reject the inconsistent result; do not reinterpret the output shape as source evidence.
- Return a quote that is not present in the cited OCR span, or a number that conflicts with deterministic parsing of that span. Keep the field unresolved.
- Use a valid, evidence-backed zero but an unsupported unit. Reject it under the field's domain rules; schema validity does not replace unit validation.
What this contract can and cannot establish
This is a design example using a fictional stock sheet, not a report of an implemented product workflow or executed extraction tests. Structured output can constrain shape, and deterministic checks can reject contradictions. Neither proves that the source was transcribed correctly. Keep uncertain values out of writes until a person checks the original evidence.
Check the provider schema as well
OpenAI's current Structured Outputs guide says its schemas require every field to be present and allow null to represent an optional-like value. It also documents a supported subset of JSON Schema and notes that structured output can still contain mistakes. Handle refusals and incomplete responses before reading extraction fields; neither is a finding about the submitted stock sheet. Treat the tagged states here as an application contract; check how your chosen provider represents it instead of assuming every JSON Schema feature is accepted.