Last reviewed September 1, 2026

How do I turn PDFs into JSON my code can validate?

Direct answer

Write the schema first. A PDF has no data model — it describes marks on a page, not fields — so the only thing that makes extracted output trustworthy is a contract you wrote before you saw the file: field names, types, which ones must be present, and what values are legal. Then keep two steps apart: extraction produces a candidate object, and validation is a separate mechanical gate that either accepts it or routes it to a human. Anything that skips the second step is guessing.

01

"PDF to JSON" is two problems wearing one coat

The first problem is getting characters off the page. A PDF may carry real text objects you can read directly, or it may be a scanned image where the characters only exist as pixels. Those are completely different jobs: one is parsing, the other is optical character recognition.

The second problem is meaning. Even with perfect characters, "4,125.60" printed near the word "Total" is not yet a field called total with the number 4125.6 in it. Mapping text to named, typed fields is the step that turns a document into data, and it is the step a schema defines.

02

Decide which kind of PDF you have

Run a quick test on a representative sample: if a text extractor returns nothing, you have image-only pages. Many real-world corpora are mixed — a native PDF invoice this month, a scan of a print of the same invoice next month — and a pipeline that only handles one kind will fail intermittently, which is the most expensive kind of failure to debug.

Assume mixed input and design for it. The schema does not change between the two cases; only what happens before mapping does.

03

Write the schema before you look at the documents

Writing the schema from the destination — the table you will insert into, the API you will post to — keeps the field list honest. Written from the document, schemas grow: someone notices a reference number in the corner, adds it, and now every extraction has one more thing that can be wrong.

A field earns its place if a downstream system reads it. Everything else is a comment.

  • Name fields after the destination column, not after the label printed on the page.
  • Type everything: dates as date strings, money as numbers without symbols or thousands separators, codes as constrained strings or enums.
  • Mark a field required only when its absence must stop the workflow.
  • Write a one-line description per field, phrased as an instruction to a careful stranger. This is the highest-leverage text in the whole schema.
  • Add ranges where you know them: a non-negative total, a date after your company was founded, a three-letter currency code.
04

Validate twice: shape, then business rules

A JSON Schema validator answers one question: does this object have the right shape and types? That is necessary and not sufficient. A perfectly shaped invoice can still say the line items sum to 3,820 while the subtotal says 3,280 — a transposition that no schema keyword catches.

So run two gates. Gate one is the schema, in code, with a real validator. Gate two is your business rules: arithmetic, cross-field consistency, duplicates, and range checks against history. Only objects that clear both should reach a system of record; the rest belong in a review queue with the reason attached.

On the Dokyumi side, the response mirrors that split. Every extraction returns data, a model-reported confidence map, and a validation block containing valid, errors, and low_confidence_fields, with an overall status of completed or review. The status is a summary of type validation and reported confidence — it is not a claim that every value matches the source document, and your own business rules still belong in your code.

Copy-pasteable artifact

Schema, expected output, and the validator that joins them

A complete, runnable example for a purchase order. Save the first pane, drop the second into a fixture file, and run the third with Ajv. You now have a regression test for extraction quality that runs in your CI and belongs entirely to you.

purchase-order.schema.json
{
  "$schema": "https://json-schema.org/draft/2020-12/schema",
  "$id": "https://example.com/schemas/purchase-order.json",
  "title": "Purchase order",
  "type": "object",
  "required": ["po_number", "buyer", "supplier", "order_date", "currency", "total", "lines"],
  "additionalProperties": false,
  "properties": {
    "po_number":  { "type": "string", "pattern": "^PO-[0-9]{4,10}$" },
    "buyer":      { "type": "string", "minLength": 1 },
    "supplier":   { "type": "string", "minLength": 1 },
    "order_date": { "type": "string", "format": "date" },
    "need_by":    { "type": ["string", "null"], "format": "date" },
    "currency":   { "type": "string", "pattern": "^[A-Z]{3}$" },
    "total":      { "type": "number", "exclusiveMinimum": 0 },
    "lines": {
      "type": "array",
      "minItems": 1,
      "items": {
        "type": "object",
        "required": ["sku", "quantity", "unit_price", "line_total"],
        "additionalProperties": false,
        "properties": {
          "sku":        { "type": "string", "minLength": 1 },
          "quantity":   { "type": "number", "exclusiveMinimum": 0 },
          "unit_price": { "type": "number", "minimum": 0 },
          "line_total": { "type": "number", "minimum": 0 }
        }
      }
    }
  }
}

Note what gate two catches that gate one cannot: quantity times unit price disagreeing with the line total, and lines that do not sum to the order total. Both objects are perfectly shaped JSON. Only arithmetic tells you one of them is wrong.

Decision table

Which check belongs where

A quick map from the thing you want to guarantee to the mechanism that can actually guarantee it. Putting a rule in the wrong column is why pipelines feel safe and are not.

What you want to guaranteeWhere it belongsWhat it looks like
The field exists and is a numberJSON Schema"type": "number", listed in required
The date is a real calendar dateJSON Schema + format assertion"format": "date" with format validation enabled
The code is one of four valuesJSON Schema"enum": ["A", "B", "C", "D"]
No unexpected fields sneak inJSON Schema"additionalProperties": false
Line items sum to the totalYour codeInteger-cent comparison with a per-line tolerance
This invoice was not already paidYour databaseUnique index on (vendor, invoice_number)
The amount is plausible for this vendorYour code + historyCompare to the trailing median; flag order-of-magnitude jumps
A human looked at the risky onesYour workflowA review queue fed by rule failures and low reported confidence

Terminology bridge

This is called schema-driven extraction, and the output contract is a JSON Schema

The general category is intelligent document processing; the specific technique of declaring the output shape up front is schema-driven or schema-first extraction, and model vendors call the same idea structured outputs. JSON Schema is the vocabulary those contracts are written in, and it is worth learning directly — it is the same language used for API request validation, configuration files, and form generation, so the schema you write here has a second life elsewhere.

  • schema-first extraction
  • structured outputs
  • IDP
  • JSON Schema
  • output contract

Follow-up questions

Should required fields be nullable?+
It depends on what "required" means to you. In the example above, required means the key must be present with a usable value. Dokyumi takes the other convention: a required field is always present in the output but may be null, so your code can branch on a missing value instead of a missing key. Pick one, write it down, and make your validator enforce it.
Can I send my JSON Schema to the extraction API as the request body?+
Not to Dokyumi. Schemas live on your account and are selected by slug at call time: you POST the file plus the schema slug to /api/v1/extract. The JSON Schema above is your own validation contract — the two are complementary, and keeping the validator on your side is what lets you fail an extraction that the vendor considered fine.
What does a completed status actually guarantee?+
That type validation passed and no reported confidence score fell below the schema threshold, which defaults to 0.8. It is not a guarantee that every value matches the source document. Confidence values are model-reported, may omit fields, and are not calibrated probabilities — which is exactly why gate two exists.

Evidence notes

Sources and limitations

Sources used

Limitations

  • A schema constrains shape, not truth. Nothing in JSON Schema can tell you a correctly typed number was read off the wrong line.
  • Dokyumi returns Zod-based type validation and a model-reported confidence map. Business-rule validation, deduplication, and approval routing remain yours.
  • Format assertion is opt-in in most validators. If you rely on "format": "date", turn assertion on or the keyword is decorative.

Published and last reviewed September 1, 2026. Product behavior can change; the linked API, pricing, and security pages are the controlling public references.

Put a real document through it

Write the schema, send one file, and diff the response against your fixture. The free plan includes 25 credits a month and 2 schemas, with no card required, which is enough to test this on your own documents before deciding anything.