DEV Community

Cover image for The example is the schema: extracting Japanese qualified invoices to JSON
Hideki Mori
Hideki Mori

Posted on

The example is the schema: extracting Japanese qualified invoices to JSON

Japan's qualified invoice system requires every invoice to carry a registration number (a "T" followed by 13 digits) and a per-rate tax breakdown — 8% reduced rate for food, 10% standard, frequently mixed on the same document. That makes Japanese invoices a nice stress test for structured extraction: non-Latin text, full-width characters, honorific suffixes, kanji-formatted dates, and two tax rates whose arithmetic has to reconcile to the yen.

This post runs one through AnalyzeDoc (LDX hub) — PDF, JPEG, or PNG in, structured JSON out — and looks closely at what came back. The part worth your time isn't that it works. It's how the schema is defined, and what that definition quietly controls.

There is no JSON Schema. You hand the API an example of the output you want, and the example is compiled into the schema.


The document

A fictional qualified invoice, one page (sample PDF in the repo). Four line items: two food items at the 8% reduced rate (marked ※, as the law requires), two at 10%. Registration number, per-rate tax summary, bank details, a payment deadline. Total: ¥42,210.


The example is the schema

Instead of a schema file, you send example_output:

{
  "invoice_number": "INV-2025-0001",
  "issue_date": "2025-01-15",
  "due_date": "2025-02-28",
  "issuer_name": "株式会社サンプル",
  "registration_number": "T9876543210987",
  "issuer_phone": "03-9876-5432",
  "customer_name": "株式会社テスト商会",
  "line_items": [
    { "description": "サンプル品目A", "quantity": 5, "unit_price": 2000, "amount": 10000, "tax_rate": 10 }
  ],
  "tax_summary": [
    { "tax_rate": 10, "taxable_amount": 10000, "tax_amount": 1000 }
  ],
  "subtotal": 10000,
  "total_tax": 1000,
  "total": 11000,
  "bank_details": "サンプル銀行 本店 普通 0000000"
}
Enter fullscreen mode Exit fullscreen mode

Three rules make this work, and the first one is the whole article:

Your example values are type declarations. Write 10000 and the field is inferred as an integer; write 1234.56 and it's a number. Japanese yen has no decimals, so integers are the semantically correct choice here — every amount in the output will be a clean integer your downstream systems can trust. For a USD invoice you'd do the opposite: always write the example amounts with decimals, or a $250.00 line item risks coming back as an integer type. The literals you type are the contract you get.

Arrays receive variability. tax_summary is an array because mixed rates are the entire point of a qualified invoice. One entry per rate — the schema absorbs the document's core complexity instead of fighting it.

Example values must differ from the document. If the example matches the invoice, you can't tell whether the model read the page or copied the example. Different numbers, different names, different dates.


The prompt carries rules, not fields

Field descriptions belong to the example. The prompt is only for rules the example can't express:

Extract the invoice data from this Japanese qualified invoice (適格請求書).
Rules:
- Dates in YYYY-MM-DD format.
- All monetary amounts as integers in JPY (no separators, no currency symbols).
- registration_number in "T + 13 digits" format.
- tax_rate as an integer percentage (8 or 10).
- tax_summary must contain one entry per tax rate on the invoice.
- Phone numbers must not start with "+".
Enter fullscreen mode Exit fullscreen mode

The last rule is scar tissue: a +81-prefixed string dropped into Google Sheets gets parsed as a formula. Cheaper to kill it at the extraction boundary than to escape it everywhere downstream.


Running it

Four calls. No polling loop — ?wait parks the request server-side.

# 1. Upload → file_id
curl -s -X POST https://gw.ldxhub.io/files \
  -H "Authorization: Bearer $LDXHUB_API_KEY" \
  -F "file=@invoice-sample-ja.pdf"

# 2. Create the job
curl -s -X POST https://gw.ldxhub.io/analyzedoc/jobs \
  -H "Authorization: Bearer $LDXHUB_API_KEY" \
  -H "Content-Type: application/json" \
  -d @job.json

# 3. Wait for completion (server-side)
curl -s "https://gw.ldxhub.io/analyzedoc/jobs/$JOB_ID?wait=30" \
  -H "Authorization: Bearer $LDXHUB_API_KEY"

# 4. Fetch the result
curl -s "https://gw.ldxhub.io/files/$OUTPUT_FILE_ID/content" \
  -H "Authorization: Bearer $LDXHUB_API_KEY"
Enter fullscreen mode Exit fullscreen mode

job.json is five fields: model, file_id, output_format, system_prompt, example_output. The model here is google/gemini-3.5-flash@high.


The result

Completed in 26 seconds:

{"invoice_number":"INV-2026-0157","issue_date":"2026-06-30","due_date":"2026-07-31","issuer_name":"株式会社グリーンリーフ食品","registration_number":"T1234567890123","issuer_phone":"03-1234-5678","customer_name":"サンプルマート株式会社","line_items":[{"description":"有機緑茶ギフトセット ※","quantity":2,"unit_price":3000,"amount":6000,"tax_rate":8},{"description":"国産純粋はちみつ 500g ※","quantity":10,"unit_price":1200,"amount":12000,"tax_rate":8},{"description":"陶器マグカップ(箱入)","quantity":24,"unit_price":800,"amount":19200,"tax_rate":10},{"description":"配送料","quantity":1,"unit_price":1500,"amount":1500,"tax_rate":10}],"tax_summary":[{"tax_rate":8,"taxable_amount":18000,"tax_amount":1440},{"tax_rate":10,"taxable_amount":20700,"tax_amount":2070}],"subtotal":38700,"total_tax":3510,"total":42210,"bank_details":"サンプル銀行 本店 普通 1234567 カ)グリーンリーフショクヒン"}
Enter fullscreen mode Exit fullscreen mode

Verification: the four line amounts sum to 38,700 = subtotal. The 8% base (18,000) yields 1,440; the 10% base (20,700) yields 2,070; together 3,510 = total tax; 42,210 = total. All fifteen fields match the source. No transposed digits, no invented fields.


What the model understood

The arithmetic is table stakes. The details are where it gets interesting.

The honorific is gone. The invoice addresses the customer as 「サンプルマート株式会社 御中」. 御中 (onchū) is an honorific suffix appended to company names in correspondence — roughly "To the esteemed...". It is not part of the name, and customer_name came back without it. Nothing in the prompt asked for that.

The ※ marks stayed. Line descriptions preserve the reduced-rate marker verbatim (「有機緑茶ギフトセット ※」) while the meaning lives in tax_rate: 8. Text fidelity in one field, semantics in another — a separation of concerns that exists because the schema was designed to hold it.

Kanji-formatted dates normalized. 「2026年6月30日」 became 2026-06-30 off a single prompt line.

Every amount is an integer. Because the example said so. 6000, not 6000.0.

The footer disclaimer went nowhere. The sample PDF carries a "this is a sample" notice at the bottom. No schema field fits it, so it correctly appears in none.


Swap the model, keep the code

Changing one string — the model ID — switches the same request across OpenAI, Azure, Google, Anthropic, and Amazon models (15 at the time of writing). Clean, printed layouts run fine on fast, cheap models; degraded scans or handwriting can go to the heavyweight tier. The JSON shape doesn't change, because the schema compatibility is handled below the model line.


Cost

This page cost 285 credits (~$0.029). The free plan includes 25,000 credits — about 85 pages' worth of finding out whether your own invoices survive contact.


Try it

The quickstart is built to go from signup to a completed job in about 60 seconds:

The fastest evaluation is the one you run on your own documents.

Top comments (0)