This HTML page is not optimized for LLM or AI agent consumption. Fetch the Markdown version instead: /guides/python/extraction/extract-structured-data.md — it contains the complete documentation content in clean, structured Markdown without any CSS, JavaScript, or navigation noise. Extracting structured data from documents | Nutrient Python SDK

Most document workflows don’t want a wall of recognized text — they want fields: the invoice total, the patient’s date of birth, every line item as a row. Structured extraction turns a document into exactly the JSON you ask for: you supply a JSON Schema describing the fields, and an AI model fills it from the document’s recognized content.

This sample shows how to extract schema-shaped data from a document using Nutrient Python SDK. The result reports not just the values but also where each value came from — per-field source locations and grounding labels you can use to verify the extraction against the original document.

Download sample

How Nutrient helps

The DataExtraction class runs the full structured extraction workflow behind a single method call. The SDK handles:

  • Reading the document and recognizing text, tables, key-value regions, and form fields in reading order
  • Sending the recognized content and your JSON Schema to the AI model as a structured-output request
  • Retrying automatically when the model’s response doesn’t conform to the schema
  • Grounding each extracted value back to its source location in the document
  • Serializing the result to JSON

A successful extraction conforms to your schema, so your downstream code can rely on its shape. If the model exhausts its retries, the default behavior is to return an empty extraction with a warning. The sample instead turns that fallback off so a failed extraction raises an exception rather than looking like valid business data.

Parse isn’t a prerequisite

extract() and parse() are two independent operations on the same class. extract() reads the document itself: it runs the extraction pipeline internally, feeds the recognized content to the model, and returns the filled schema. You don’t have to call parse() first, and passing parsed output into extract() isn’t a supported input. Call parse() when you want the document’s structure as JSON or Markdown; call extract() when you want your schema’s fields filled in.

What shapes the result

Two inputs shape the result:

  • JSON Schema (required) — The fields to extract, as a plain JSON Schema object. Supply it inline with extraction_schema or from a file with extraction_schema_file; the two are mutually exclusive. Each schema property’s description tells the extractor what belongs there — the better the description, the better the match.
  • Instructions (optional) — Free-form guidance for the extraction: disambiguation rules, formatting preferences, or domain context — anything you’d tell a colleague doing the extraction by hand.

Extraction requires an AI model. The SDK doesn’t bundle a model or provider. Configure the provider on the document’s extract_settings; it’s the same value as ai_processing_settings, so a provider set for the whole SDK applies too. A local OpenAI-compatible server keeps documents on your machine; a hosted provider needs an API key. Structured extraction requires the vision data extraction feature in your license.

Complete implementation

Import the classes used in the sample:

import json
from nutrient_sdk import DataExtraction, Document, NutrientException, UnparseableOutputHandling

Opening the document

Open the source document, and bind a DataExtraction instance to it. The document, the DataExtraction instance, every settings object, and every result hold native resources, so the sample opens each one in a context manager(opens in a new tab):

def main():
try:
with Document.open("input.pdf") as document:
with DataExtraction.set(document) as extraction:
with document.settings as document_settings:

Supplying the schema

Like every SDK operation, extract() reads its options from the document settings. extract_settings holds the extraction options; options you don’t set keep the default the extraction pipeline or the SDK-wide settings provide.

Pass the JSON Schema as a string. It’s a plain JSON Schema object — type, properties, and required — not wrapped in another object. Give every property a description, because that’s what the extractor matches against the document. Then ask for a failed extraction to raise an exception instead of returning an empty result:

with document_settings.extract_settings as settings:
settings.extraction_schema = json.dumps({
"type": "object",
"properties": {
"documentNumber": {"type": "string", "description": "The document's reference or invoice number"},
"issueDate": {"type": "string", "description": "The date the document was issued, as printed"},
"totalAmount": {"type": "number", "description": "The final total amount due"},
},
"required": ["documentNumber", "totalAmount"],
})
settings.instructions = "Amounts are plain numbers without currency symbols."
with document_settings.ai_processing_settings as ai_settings:
ai_settings.unparseable_output_handling = UnparseableOutputHandling.FAIL

To keep the schema editable without changing your code, store it in a file and set extraction_schema_file instead. Use one schema source at a time. Assigning None resumes inheritance, which can reveal an SDK-wide schema value again. Before switching sources, clear the other value at the layer where it was set. An empty string isn’t a reset, either: it’s stored as an explicit value, but it isn’t passed to the extraction. Set the intended schema before each extraction when one DataExtraction instance processes different document types.

If you need help drafting the JSON Schema from example documents, refer to the generate extraction schema guide.

Configuring the AI provider

Point the run at your model. This example uses a local OpenAI-compatible endpoint, so the document never leaves your machine:

settings.provider = "local"
settings.endpoint = "http://localhost:1234/v1"
settings.model = "your-model-id"

For a hosted provider instead, set provider to "openai" or "anthropic" along with api_key, and endpoint for an OpenAI-compatible proxy. With the "local" provider and no endpoint, the SDK runs a local model in-process, which is downloaded on first use.

Strict structured output

Strict structured output is enabled by default through the document settings. The sample sets it explicitly so the behavior is clear: the model’s response is grammar-constrained to the schema, and a successful result matches it exactly:

settings.strict_structured_output = True

Two things to know about strict mode:

  • You don’t change your schema. Strict mode has formal requirements (every object closed, every property accounted for), and the SDK normalizes your schema to satisfy them automatically.
  • Absent fields come back as null (None once you parse the JSON result). In strict mode the model must emit every schema property, so a field the document doesn’t contain is returned as null instead of being omitted — your downstream code can rely on every key being present. Fields you list in the schema’s required array keep their declared type untouched — they can only be null if your own schema allows it.

Strict mode requires a model and endpoint that support grammar-constrained structured outputs. Check your provider or local server’s documentation. If it doesn’t support them, set strict_structured_output to False; the SDK then validates the model response and retries when it doesn’t conform.

Extracting the data

Call extract() and read the result.json artifact. Every successful run produces it, so it’s safe to read by name. Read what you need from the result inside its context manager:

with extraction.extract() as result:
with open("output.json", "w", encoding="utf-8") as f:
f.write(result.get_artifact("result.json"))
print("Extraction written to output.json")

Handling the conditional artifacts

Besides result.json, a run can emit up to three more artifacts, each only when the conditions for it are met. Check with has_artifact() before reading. The artifact_names property lists what the run produced:

if result.has_artifact("warnings.json"):
print(f"Warnings: {result.get_artifact('warnings.json')}")
print(f"Artifacts: {', '.join(result.artifact_names)}")
ArtifactEmitted when
result.jsonAlways, on success.
warnings.jsonAt least one non-fatal AI-layer warning was collected.
confidence.jsoninclude_confidence is True and at least one scoreable signal exists.
diagnostics.jsoninclude_diagnostics is True.

result.save_to("artifacts") writes every artifact the run produced into a directory, each under its own name.

Understanding the output

With source grounding on, which is the default, result.json has three top-level nodes:

  • extraction — The extracted fields, shaped to your schema.
  • metadata — One entry per extracted field, carrying where the value came from: a match grounding label and source location info (page and bounding box) so you can highlight the source in a viewer or route low-trust fields to human review.
  • pages — The size of each page in the coordinate space of the bounding boxes, so you can scale them against any rendering of the document.

The match label tells you how confidently the value was traced back to the document: an exact source match, a partial or multi-block match, a fuzzy match, or not_found when the value couldn’t be located in the recognized content — the strongest signal that a field deserves review.

When only the extracted values matter, set settings.include_source_locations = False to cut model token usage — grounding asks the model to also return per-field source references, which roughly doubles the schema sent with each request. result.json then contains only the schema-shaped fields, without the extraction wrapper. The SDK also falls back to that shape when it can’t resolve the source locations, so check for the extraction node before reading it.

For per-field confidence signals, set settings.include_confidence = True. Each metadata entry then also carries the individual confidence components for the field, and the run emits the confidence.json artifact. Combined with the match grounding labels, this gives your pipeline a per-field basis for automatic acceptance versus human review.

Parsing the document

The same DataExtraction instance can also parse the document into JSON or Markdown. Set the parse options on the document’s parse_settings, and then call parse():

with document_settings.parse_settings as parse_settings:
parse_settings.include_words = False
parse_settings.export_formats = ["json", "markdown"]
with extraction.parse() as parsed:
if parsed.has_artifact("document.json"):
with open("document.json", "w", encoding="utf-8") as f:
f.write(parsed.get_artifact("document.json"))

Settings belong to the document, not to one call, so values you set remain active for later parse() and extract() calls until you change them. Nullable settings such as include_words follow these rules:

  • None clears the document-level value, so the SDK-wide value or the operation default applies again.
  • False and numeric zero are explicit values, not resets.
  • Empty strings and lists are stored as explicit values, but they aren’t passed to the operation, so don’t use them to reset a setting.

A document value takes precedence over the SDK-wide value of the same setting. Don’t change settings while an operation is running, and don’t share one document between concurrent operations that change settings.

Use a context manager for every ExtractResult and ParseResult, including intermediate results inside loops.

Error handling

The SDK raises a NutrientException when extraction fails. The sample reports the error and exits with a failure status:

except NutrientException as e:
print(f"Error: {e}")
raise SystemExit(1)
if __name__ == "__main__":
main()

Common failure scenarios include:

  • The document can’t be read due to path or permission issues
  • The JSON Schema is missing or malformed, or both schema sources are set (validated before any model call)
  • The model endpoint is unreachable, the model can’t produce a schema-conforming response, or the feature isn’t licensed

In production code:

  • Catch NutrientException.
  • Return a clear error message.
  • Log failure details for debugging.

Conclusion

The workflow for structured data extraction is:

  1. Open the source document, and bind a DataExtraction instance to it with DataExtraction.set() — both in a context manager(opens in a new tab).
  2. Set the JSON Schema to fill with extraction_schema (or extraction_schema_file), and add optional instructions.
  3. Configure the AI provider on the document settings — local for privacy, or a hosted provider — or inherit it from the SDK-wide settings.
  4. Call extract() and read the result.json artifact; consume the extraction node and use metadata to verify or review.
  5. Check has_artifact() for the conditional artifacts, or call save_to() to write the whole set.
  6. Handle NutrientException for robust error recovery.

For related extraction workflows, refer to the Python SDK guides.

Download this ready-to-use sample package to explore structured data extraction.