Extracting structured data from documents
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 Java 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 sampleHow 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 throws 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
setExtractionSchema()or from a file withsetExtractionSchemaFile(); the two are mutually exclusive. Each schema property’sdescriptiontells 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 ExtractSettings; it’s the same value as AiProcessingSettings, 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
Specify a package name and create a new class:
package io.nutrient.Sample;Import the classes used in the sample:
import io.nutrient.sdk.DataExtraction;import io.nutrient.sdk.Document;import io.nutrient.sdk.enums.UnparseableOutputHandling;import io.nutrient.sdk.exceptions.NutrientException;import io.nutrient.sdk.results.ExtractResult;import io.nutrient.sdk.results.ParseResult;import io.nutrient.sdk.settings.DocumentSettings;import io.nutrient.sdk.settings.ExtractSettings;import io.nutrient.sdk.settings.ParseSettings;
import java.io.IOException;import java.nio.file.Files;import java.nio.file.Path;Opening the document
Open the source document and bind a DataExtraction instance to it. Both implement AutoCloseable, so open them in a single try-with-resources block. The document releases the file, and the DataExtraction instance releases the native handle that pins the document. Settings views and result objects aren’t AutoCloseable, so use them as ordinary Java objects:
public class ExtractStructuredData { public static void main(String[] args) { try (Document document = Document.open("input.pdf"); DataExtraction extraction = DataExtraction.set(document)) {Supplying the schema
Like every SDK operation, extract() reads its options from the document settings. ExtractSettings 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 throw instead of returning an empty result:
DocumentSettings documentSettings = document.getSettings(); ExtractSettings settings = documentSettings.getExtractSettings(); settings.setExtractionSchema( "{" + " \"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.setInstructions("Amounts are plain numbers without currency symbols."); documentSettings.getAiProcessingSettings().setUnparseableOutputHandling(UnparseableOutputHandling.Fail);To keep the schema editable without a rebuild, store it in a file and call setExtractionSchemaFile(path) instead. Use one schema source at a time. Setting a value to null 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.setProvider("local"); settings.setEndpoint("http://localhost:1234/v1"); settings.setModel("your-model-id");For a hosted provider instead, set the provider to "openai" or "anthropic" along with setApiKey(), and setEndpoint() 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.setStrictStructuredOutput(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. In strict mode the model must emit every schema property, so a field the document doesn’t contain is returned asnullinstead of being omitted — your downstream code can rely on every key being present. Fields you list in the schema’srequiredarray keep their declared type untouched — they can only benullif 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, call setStrictStructuredOutput(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:
ExtractResult result = extraction.extract(); Files.writeString(Path.of("output.json"), result.getArtifact("result.json")); System.out.println("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 hasArtifact() before reading, and use getArtifactNames() to list what the run produced:
if (result.hasArtifact("warnings.json")) { System.out.println("Warnings: " + result.getArtifact("warnings.json")); } System.out.println("Artifacts: " + String.join(", ", result.getArtifactNames()));| Artifact | Emitted when |
|---|---|
result.json | Always, on success. |
warnings.json | At least one non-fatal AI-layer warning was collected. |
confidence.json | setIncludeConfidence(true) and at least one scoreable signal exists. |
diagnostics.json | setIncludeDiagnostics(true). |
result.saveTo("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: amatchgrounding 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, call settings.setIncludeSourceLocations(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, call settings.setIncludeConfidence(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 ParseSettings, and then call parse():
ParseSettings parseSettings = documentSettings.getParseSettings(); parseSettings.setIncludeWords(Boolean.FALSE); parseSettings.setExportFormats(new String[] {"json", "markdown"}); ParseResult parsed = extraction.parse(); if (parsed.hasArtifact("document.json")) { Files.writeString(Path.of("document.json"), parsed.getArtifact("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 setIncludeWords() follow these rules:
nullclears the document-level value, so the SDK-wide value or the operation default applies again.Boolean.FALSEand numeric zero are explicit values, not resets.- Empty strings and arrays 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.
ExtractResult and ParseResult don’t have a close() method. The SDK releases their native resources after the objects become unreachable. Read the artifacts you need, and let intermediate results go out of scope, especially inside loops.
Error handling
The SDK throws a NutrientException when extraction fails. The sample also catches IOException for writing the output files:
} catch (NutrientException | IOException e) { System.err.println("Error: " + e.getMessage()); System.exit(1); } }}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:
- Open the source document, and bind a
DataExtractioninstance to it withDataExtraction.set()— both in a try-with-resources block. - Set the JSON Schema to fill with
setExtractionSchema()(orsetExtractionSchemaFile()), and add optional instructions. - Configure the AI provider on the document settings — local for privacy, or a hosted provider — or inherit it from the SDK-wide settings.
- Call
extract()and read theresult.jsonartifact; consume theextractionnode and usemetadatato verify or review. - Check
hasArtifact()for the conditional artifacts, or callsaveTo()to write the whole set. - Handle
NutrientExceptionfor robust error recovery.
For related extraction workflows, refer to the Java SDK guides.
Download this ready-to-use sample package to explore structured data extraction.