Guide

Extract PDF data into Zod and Pydantic models

All guides

A PDF-to-JSON integration has two separate jobs: produce an object your application can use, and preserve enough evidence to decide whether to use it. This guide extracts an invoice into a Zod or Pydantic model while retaining field states and source references. A value passing type validation can still be the wrong value from the document.

Use a local invoice PDF you are allowed to process and a Velrim API key. The examples make a real extraction call when you run them; page billing applies. They print only request metadata. In your application, consume the returned object directly rather than logging document contents.

TypeScript: a PDF to a validated Zod object

In a Node project, run npm install @velrim/sdk zod and npm install --save-dev tsx. Set VELRIM_API_KEY in your environment. Save the download as invoice.ts, then run npx tsx invoice.ts invoice.pdf with Node 22 or later.

The required invoice fields are strings, a number and a currency enum. A purchase-order number is allowed to be absent or explicitly null. The SDK returns result.data after parsing it with this Zod model; the fields map remains a separate object. Download invoice.ts.

invoice.ts
import { readFile } from 'node:fs/promises';
import { pathToFileURL } from 'node:url';
import { z } from 'zod';
import { VelrimClient, type VelrimClientOptions } from '@velrim/sdk';
export const Invoice = z.object({
invoice_number: z.string(),
total: z.number(),
currency: z.enum(['USD', 'EUR']),
po_number: z.string().nullable().optional(),
});
export async function extractInvoice(bytes: Uint8Array, options: VelrimClientOptions = {}) {
const client = new VelrimClient(options);
const result = await client.extract(Invoice, { bytes });
// Validation enforces types. Field metadata describes extraction evidence.
return { invoice: result.data, fields: result.fields, meta: result.meta };
}
if (process.argv[1] && import.meta.url === pathToFileURL(process.argv[1]).href) {
const path = process.argv[2];
if (!path) throw new Error('Usage: npx tsx invoice.ts invoice.pdf');
const result = await extractInvoice(await readFile(path));
// Content-free output; inspect invoice and fields inside your application.
console.log({ request_id: result.meta.request_id, billed_pages: result.meta.billed_pages });
}

Python: a PDF to a Pydantic instance

In a Python virtual environment, run python -m pip install velrim and set VELRIM_API_KEY. Save this file as invoice.py, then run python invoice.py invoice.pdf. The typed object is result.parsed; result.data and result.fields remain available.

Optional[str] = None accepts both omission and null. That is convenient for application code, but the parsed attribute alone cannot tell which extraction state occurred. Inspect result.fields before making a decision that depends on absence. Download invoice.py.

invoice.py
import sys
from typing import Literal, Optional
from pydantic import BaseModel
from velrim import Client, Document
class Invoice(BaseModel):
invoice_number: str
total: float
currency: Literal["USD", "EUR"]
po_number: Optional[str] = None
def extract_invoice(path: str):
with Client() as client: # reads VELRIM_API_KEY
result = client.extract(document=Document.from_path(path), schema=Invoice)
# The default None folds omission and explicit null together in parsed.
# Keep fields to distinguish their extraction states.
return result
if __name__ == "__main__":
if len(sys.argv) != 2:
raise SystemExit("Usage: python invoice.py invoice.pdf")
result = extract_invoice(sys.argv[1])
print({"request_id": result.meta.request_id, "billed_pages": result.meta.billed_pages})

Keep present, null and missing separate

Use the RFC 6901 pointer /po_number to look up the field entry. Optional and nullable describe what the model accepts. The extraction state describes what the API returned. Do not turn every absent value into an empty string or zero: that erases information and can make a failed extraction look like a valid amount.

Keep present, null and missing separate
Field stateMeaning for the callerHandling in this invoice example
presentA value was returned; inspect confidence and grounding separately.Pass the typed value to business checks.
nullThe field is explicitly null.Allow it for po_number; decide whether the workflow needs review.
missingThe field could not be extracted; inspect reason.Keep the omission visible and route required information to review.
No field entryThere is no metadata at the pointer you requested.Check the pointer and schema; do not treat absent metadata as a pass.

Handle validation failure without inventing defaults

A required field may be missing even in an HTTP 200 extraction response. If the returned data cannot satisfy your model, the TypeScript SDK raises SchemaMismatchError; the Python model path can raise Pydantic ValidationError. Catch these at your task boundary and send the document reference to review. An extraction-level HTTP error is a different failure: follow the error reference.

If you need to inspect a partial response before enforcing a strict application model, use the raw JSON Schema path described in the extraction docs, retain the envelope, then validate explicitly. A TypeScript generic on the raw path does not perform runtime validation. Keep defaults and coercions out of the extraction model unless you deliberately want that transformation; Pydantic can coerce compatible inputs by default.

What the types do not establish

An invoice total is still a number when it was copied from the wrong subtotal. A source anchor points to a region; grounding: "verified" means the value was located there, not that it belongs to the requested field. Check currencies, totals, duplicate invoice IDs and the receiving account in your own application.

Next, build an exception-review workflow or use the migration checklist. For library semantics, see Zod optionals and nullables and Pydantic models. Velrim confidence measurements and their document-class limits are at /reliability.

Written by Velrim.