Skip to content

Schemas

scriva.schemas ships pydantic models for the document types people most often want to extract. Pass one to scriva.extract, a preset, or any structured-output Worker and you get a typed instance back.

import scriva
invoice = scriva.extract("acme-2026-05.pdf", schema=scriva.schemas.Invoice)
print(invoice.total_amount, invoice.currency)
for line in invoice.line_items:
print(line.description, line.quantity, line.unit_price)

You can also subclass any of them to add fields specific to your domain — pydantic inheritance just works, and the Worker’s structured-output prompt picks up the extra fields automatically.

SchemaImport pathTypical source
Invoicescriva.schemas.Invoiceinvoices, bills
Receiptscriva.schemas.Receiptretail/restaurant receipts
IdCardscriva.schemas.IdCarddriver’s licences, national IDs
Passportscriva.schemas.Passportpassport pages (MRZ-aware)
BusinessCardscriva.schemas.BusinessCardbusiness cards
Contractscriva.schemas.Contractlegal contracts
BankStatementscriva.schemas.BankStatementbank/credit-card statements
Resumescriva.schemas.ResumeCVs / résumés
MedicalFormscriva.schemas.MedicalFormclinic intake / lab reports
Prescriptionscriva.schemas.Prescriptionprescriptions, dispensing labels
PurchaseOrderscriva.schemas.PurchaseOrderPOs, sales orders
ShippingLabelscriva.schemas.ShippingLabelshipping/return labels
Chequescriva.schemas.Chequecheques / cheque images
TabularFormscriva.schemas.TabularFormgeneric grid forms

Every schema in the table is a pydantic.BaseModel. Open one in your editor (from scriva.schemas import Invoice; Invoice.model_json_schema()) to see the full field list.

class Invoice(BaseModel):
invoice_number: str
invoice_date: date
due_date: date | None = None
currency: str # ISO 4217, e.g. "USD"
subtotal: Decimal | None = None
tax_amount: Decimal | None = None
total_amount: Decimal
vendor: Party
customer: Party | None = None
line_items: list[LineItem] = []
payment_terms: str | None = None
notes: str | None = None
class Party(BaseModel):
name: str
address: Address | None = None
tax_id: str | None = None
email: str | None = None
phone: str | None = None
class LineItem(BaseModel):
description: str
quantity: Decimal
unit_price: Decimal
total: Decimal
sku: str | None = None
tax_rate: Decimal | None = None
class Receipt(BaseModel):
merchant_name: str
merchant_address: Address | None = None
transaction_date: datetime
currency: str
subtotal: Decimal | None = None
tax_amount: Decimal | None = None
tip_amount: Decimal | None = None
total_amount: Decimal
payment_method: Literal["cash", "card", "mobile", "other"] | None = None
last4: str | None = None # card last-4, if present
line_items: list[LineItem] = []
class IdCard(BaseModel):
id_number: str
full_name: str
date_of_birth: date
sex: Literal["M", "F", "X"] | None = None
issue_date: date | None = None
expiry_date: date | None = None
issuing_authority: str | None = None
address: Address | None = None
nationality: str | None = None # ISO 3166-1 alpha-3
class Passport(IdCard):
passport_number: str
mrz_line_1: str # raw MRZ
mrz_line_2: str
place_of_birth: str | None = None

Passport.mrz_* fields preserve the raw machine-readable zone so downstream verification can re-parse it. scriva.schemas.parse_mrz(...) returns a Passport from MRZ text alone.

class BankStatement(BaseModel):
account_holder: str
account_number: str # often last-4 only
statement_period_start: date
statement_period_end: date
currency: str
opening_balance: Decimal
closing_balance: Decimal
transactions: list[Transaction]
class Transaction(BaseModel):
date: date
description: str
amount: Decimal # signed: + credit, - debit
balance: Decimal | None = None
class Contract(BaseModel):
title: str
effective_date: date | None = None
expiry_date: date | None = None
parties: list[Party]
governing_law: str | None = None
clauses: list[Clause]
class Clause(BaseModel):
heading: str | None = None
body: str

Add fields, override types, change validators — standard pydantic:

from decimal import Decimal
from scriva.schemas import Invoice, LineItem
class TaxLineItem(LineItem):
tax_rate: Decimal # was Optional in the base
class MyInvoice(Invoice):
purchase_order: str | None = None
line_items: list[TaxLineItem] = [] # narrow the type
result = scriva.extract("acme.pdf", schema=MyInvoice)

The Worker’s structured-output prompt rebuilds itself from MyInvoice.model_json_schema() on each call, so subclassing is the canonical way to localise a schema to your vendor mix.

You don’t need a built-in — any pydantic.BaseModel works. Match the field names to terms that appear in your documents and the VLM will fill them in:

from pydantic import BaseModel
from datetime import date
class LabReport(BaseModel):
patient_name: str
collection_date: date
panel: str
glucose_mg_dl: float
cholesterol_mg_dl: float
report = scriva.extract("lab-2026-05.pdf", schema=LabReport)

Field names matter more than descriptions — the model uses them verbatim in the prompt. Reach for Field(description=...) when the name alone is ambiguous (e.g. total: Decimal = Field(description="total including tax")).

Schemas validate on construction. A field declared Decimal will reject a string that isn’t a number; a date will reject "sometime in May". When a field is mandatory and the VLM doesn’t return it, the Worker raises RecognitionError rather than silently filling in None — use schema.model_fields[name].default = None (or just make the field Optional) when partial extraction is acceptable.

For tolerant-but-typed extraction, the convenience constructor scriva.schemas.lenient(MyInvoice) returns a copy of the schema with all fields demoted to Optional and validators downgraded to warnings. Use it for first-pass classification, then validate strictly on the fields you actually care about.