Scanned PDF to JSON for Structured Legal Data
By Schema LawPublished Updated
To turn a scanned PDF into JSON, run the file through OCR that keeps structure, then export the result as a JSON file. A useful export holds each page's text and its tables as separate, labelled values, so a script or database can read the document without anyone retyping or copying from the screen.
JSON is the export to pick when the next reader is a program. If a person will read the result, Word or a searchable PDF serves better. The sections below describe what the file holds, when a firm has a use for it, how to produce one, and where structured output from a scan stops.
What does JSON from a scanned document contain?
JSON from a scanned document contains the recognized content of each page, arranged as named values instead of a formatted page. In a Schema Law export that means the document's name, the upload time, a page count, and one entry per page holding that page's text, its tables and its Markdown.
JSON is a text format for data. The specification, RFC 8259, says it can represent four primitive types (strings, numbers, booleans and null) and two structured types (objects and arrays). An object is a set of name and value pairs. An array is an ordered list. Nearly every programming language and database can read both.
What matters is which names the file uses, because your script will ask for values by name. The Schema Law export has four at the top level:
| Name | What it holds |
|---|---|
name | The name of the document |
uploaded_at | The date and time of upload, written as an ISO timestamp |
page_count | The number of pages that were read |
pages | An array with one object per page, in page order |
Each object inside pages has four names of its own: page (the page number, counting from 1), text (the page as plain text), tables (every table found on that page) and markdown (the same page with its headings, lists and tables marked up).
Notice what is absent. The file has no parties, effective_date or monthly_rent. It records what is on each page and where. It does not decide what the words mean. That second step is extraction, and the post on contract data extraction covers it.
When is JSON the right export for a law firm?
JSON is the right export when software will consume the document: a script that fills a spreadsheet, a database of matter documents, a search index, or an extraction step that pulls out dates and names. Choose something else when the output goes straight to a person.
Four situations come up in practice:
- You are building a searchable store of documents. A search index wants text plus a few labels, such as the file name and page number. The JSON gives you both, page by page, so a hit can point to the page it came from.
- You want to pull out fields afterwards. Extraction tools and scripts work on text. Feeding them text that is already split by page lets every extracted value carry a page reference you can check.
- You need tables as data, with their page numbers. Each table sits in the entry for the page it was found on, so a rent schedule on page 14 stays tied to page 14.
- Your firm has a developer or a technically minded paralegal. JSON has no value until something reads it. If nobody at the firm writes scripts, a spreadsheet export will get the same table in front of you faster.
If the only goal is to load one table into another program, a CSV is simpler, and the guide to converting a scanned PDF to CSV explains that file. If the goal is to read or edit the document, export Word.
A quick test: write down the first thing that will open the file. If the answer is Excel, Word or a person, JSON is the wrong choice. If the answer is Python, a database import or an internal tool, it is the right one.
If the scan is a lease, have the script pull the landlord, the tenant, the rent and the term, each with its page number. A security deposit is worth a field only when the page states one. The JSON feeds that table, and you still check every value against the image before anyone relies on a row.
From scanned page to JSON export
Getting from a scanned page to a JSON export takes three actions: upload the scan, check the parsed pages on screen, and download the JSON. The check in the middle is the one people skip, and it is the one that saves a bad import later.
- Start with the file as it arrived. Schema Law accepts PDF, PNG and JPEG files up to 20 MB. A phone photo of a single page counts as a one-page document.
- Upload it from the browser. You sign in with Google and add the file. The pages are read into text and tables.
- Look at the parsed pages before exporting. Find the pages that matter most, usually the ones with tables or numbered clauses, and compare them with the scan. A table that looks wrong on screen will be wrong in the JSON too.
- Export JSON. Pick JSON from the export choices for the document. You can export the same document again in another format later, so it costs nothing to take a Word copy for the reviewing lawyer as well.
- Keep the scan beside the JSON. Store both under the same base name. The JSON is a reading of the scan. The scan stays the record.
JSON is one of the export formats on every plan, the free one included, so the plans differ in pages and not in formats. The pricing page lists the page limits for each plan. To judge the output on your own documents before paying, run a short one through the free plan, which takes one document a day of up to three pages, and open the JSON itself. Pick a page with a table, since tables are where the structure is tested hardest.
Reading the exported structure: pages, headings, tables
The exported structure is a list of pages. Each page gives you its text three ways: as plain text, as tables, and as Markdown in which headings are marked. Here is a shortened two-page example with invented parties and figures.
{
"name": "HARLOW-0004127.pdf",
"uploaded_at": "2026-10-08T15:04:05.000Z",
"page_count": 2,
"pages": [
{
"page": 1,
"text": "COMMERCIAL LEASE AGREEMENT\n\nThis Lease is entered into as of March 3, 2021, by and between ...",
"tables": [],
"markdown": "# COMMERCIAL LEASE AGREEMENT\n\nThis Lease is entered into as of March 3, 2021, by and between ..."
},
{
"page": 2,
"text": "Rent Schedule\n\nLease year\tMonthly rent\n1\t$18,450.00\n2\t$19,003.50",
"tables": [
{
"header": ["Lease year", "Monthly rent"],
"rows": [
["1", "$18,450.00"],
["2", "$19,003.50"]
]
}
],
"markdown": "## Rent Schedule\n\n| Lease year | Monthly rent |\n| --- | --- |\n| 1 | $18,450.00 |\n| 2 | $19,003.50 |"
}
]
}Pages
The page number is the position of the page in the uploaded file, starting at 1. It is not the number printed at the foot of the paper and it is not a Bates number. If your scan begins at page 37 of a production, "page": 1 is still the first image in the file. Record the offset when you load the data.
Text
The text value is the page with markup removed. Blocks are separated by a blank line. List items begin with a hyphen or a number. A table appears inside the text too, one row per line with a tab between cells, so a full-text search of this value will still find a figure that sits in a table.
Headings
There is no separate headings list. Headings live in the markdown value, where a heading line begins with one to six # characters. To build an outline of the document, read each page's markdown line by line and keep the lines that start with #. In the text value the same heading is an ordinary line with no marker.
Tables
Each entry in tables has a header (the first row) and rows (every row after it). Each cell is a string. Two details catch people out:
- Numbers are strings.
"$18,450.00"arrives with its dollar sign and comma. Your code has to strip them before adding anything up. - Rows can differ in length. If recognition found three cells in one row and four in the next, the JSON keeps them that way. Check row lengths before you assume column 3 is always the amount.
A page with no table has an empty tables array, not a missing one, so code can loop over it safely.
Loading document JSON into a database or script
Loading document JSON into a database or script is a matter of reading the file, walking the pages array, and writing each page or table row somewhere you can query. A dozen lines of Python is enough to start.
This script turns every table in an export into rows of a CSV, adding the page each row came from:
import csv
import json
with open("HARLOW-0004127.json", encoding="utf-8") as source:
document = json.load(source)
with open("tables.csv", "w", newline="", encoding="utf-8") as target:
writer = csv.writer(target)
for page in document["pages"]:
for number, table in enumerate(page["tables"], start=1):
for row in table["rows"]:
writer.writerow([document["name"], page["page"], number, *row])For a database, two tables cover most needs:
| Table | Columns | Filled from |
|---|---|---|
documents | name, uploaded_at, page_count | The top level of the file |
pages | document name, page, text, markdown | One row per entry in pages |
Add a third table for table cells only if you query them often. Otherwise store each page's tables value as it is, since most databases now accept a JSON column.
Three habits prevent trouble later:
- Read the file as UTF-8. RFC 8259 requires UTF-8 for JSON exchanged between systems, and legal text is full of section signs, curly quotes and accented names that break under any other encoding.
- Keep the source file name on every row.
namepluspageis your route back to the scan when someone asks where a figure came from. - Do not convert values on the way in. Store the string exactly as recognized, and put the cleaned date or amount in a second column. When a value looks wrong, you will want to see what the page reading actually said.
Before you trust any figure that came through this route, compare it with the page image. The post on why OCR output is full of errors has a section on proofreading OCR output against the original that applies to data as much as to prose.
Limits of structured output from a scan
Structured output from a scan is limited to what recognition found on the page. JSON makes that reading easy for a program to consume. It does not make the reading more accurate, and it does not know what the document means.
Keep these limits in view when you plan work around the file:
- Layout, not meaning. The export says "this is a heading" and "this is a table". It does not say "this is the governing law clause" or "this party is the tenant". Field extraction is a separate step with its own checking.
- Errors look more official. A wrong digit in a neat JSON value looks just as trustworthy as a right one. The file carries no marker for a doubtful character, so plan to check the values that matter against the scan.
- No positions on the page. The file gives the order of content within a page, not coordinates. You cannot use it to draw a highlight box over a word in the image.
- Tables are only as good as their detection. Merged cells, borderless columns and tables that run across pages can come out with shifted or split rows. A table that continues on the next page appears as two tables, one per page. The post on scanned PDF to Excel explains how OCR finds a table on a scanned page and why it sometimes misses.
- Marks that are not text are not data. A signature, a seal or a checked box is a picture. Do not expect a
signed: truevalue. - The shape is fixed. Every Schema Law JSON export uses the structure shown above. Custom export layouts are not offered on any plan, so if a system needs different names, reshape the file in your own script.
Treat the JSON as a faithful container for a reading that still needs a human check on the points that carry weight: names, dates, amounts and section numbers. With that check in place, it saves the retyping. Without it, it moves errors into your systems faster. The export formats on the homepage show the other files you can take from the same upload when a person, not a program, is the next reader.