PDF to JSON: how to actually get structured data out
Digital PDFs and scanned PDFs are different problems. Code for both routes, why page one is not the document, and the checks that catch errors before they reach your database.
7 min readConverting a PDF to JSON means two different jobs depending on the PDF you have, and picking the wrong one is why most attempts stall. This covers both, with code, and where each stops working.
Which PDF do you have
A digital PDF was produced by software and carries a text layer, the actual characters. Reading it needs no model.
A scanned PDF is images wrapped in a PDF container. There are no characters, only pixels, so it needs OCR or a vision model.
You can tell them apart by trying to extract text and seeing whether anything comes back:
A real pipeline gets both, usually in the same folder, so detect rather than assume.
Route one: the PDF has text
Pull the text out with a library and you have a string. Fast, free, no model. The problem is what you get is a string, not data, and the layout that made it meaningful is gone.
Two columns interleave into nonsense. A table becomes a run of numbers with no idea which column each belonged to. A total looks exactly like a line item. If your PDFs come from one source and never change, a few regular expressions on that text will hold, and that is a legitimate answer for a narrow, stable input.
The moment a second supplier sends a different layout, the regular expressions stop being maintainable, and you are back to needing something that understands the page rather than the character stream.
Route two: the PDF is scanned, or you need structure
Here you want a model that sees the page and returns the shape you asked for. Send the PDF, send a JSON Schema, get typed fields back:
In TypeScript, with a webhook so nothing waits on the request:
The job returns immediately with an id and runs asynchronously. When it completes, output is a JSON string you parse into your own type. Numbers come back as numbers whatever separator the document used, and adding "format": "date" to a date field makes its ISO conversion a runtime guarantee rather than something the model formats. See Schemas and formats.
If you want the whole page rather than fields, send "target": "markdown" with no schema and you get the document as structured Markdown, tables included.
Whole document, not just page one
Two things break naive implementations here.
Multi page documents. A PDF is not one page, and fields do not politely stay on page one. A total often sits on the last page while line items span three. Any approach that reads page one and stops will look like it works on your test file. Veralens takes PDFs up to 200 pages and 20 MB in a single job, so the model sees the whole document rather than a page at a time.
Time. Pages take seconds, and a bad page takes a lot longer. Our measured median is 3.4 seconds per page with a 95th percentile of 17.3. A twenty page contract is not something an HTTP request should be waiting for, which is why extraction here is a job with a webhook rather than a synchronous call. The reasoning is in why your function times out.
Then check the output
PDF to JSON is not finished when JSON appears. Two habits catch most real errors, and neither needs a model:
- Check the document against itself. Line items should sum to the subtotal, subtotal plus tax should equal the total. Documents are redundant, and that redundancy is free validation.
- Treat null as information. A field that is genuinely absent should come back null rather than filled with something plausible. That is why the required list in the schema above is short. Route nulls to review instead of forcing a value.
Which route to pick
Three of those rows are the same request with a different body, which is the point. You do not build a branch per case:
- Mixed batches: an array whose
itemsare ananyOfof your document shapes, each with akindenum. Classification and extraction happen in one pass. Worked example in how to parse unstructured data. - Typed values:
format: "date","country","iban","qrcode"and the rest resolve after extraction, in code. See Schemas and formats. - Whole pages:
target: "markdown"with no schema.
The quickstart gets you a first extraction in a few minutes, and the invoice walkthrough goes field by field.