Define what to extract
Tables and nested data
Line items, transactions, and grouped values. How to model repeating data so it comes back as rows instead of a mess.
Most documents worth extracting have something that repeats — invoice line items, statement transactions, packing list contents. Modelling that correctly is the difference between data you can use and data you have to clean.
The mistake to avoid
If a document has a list of things, do not create numbered fields:
item_1_description, item_1_quantity, item_1_price
item_2_description, item_2_quantity, item_2_price
item_3_description, item_3_quantity, item_3_price
This breaks the moment an invoice has four lines. You get empty fields on short documents, truncation on long ones, and a schema nobody can maintain.
Use a Table field instead. Define the columns once and get one row per line actually found — three rows, or forty.
Table fields
A Table field holds repeating rows. You define the columns; Parsli finds the rows.
For invoice line items:
| Column | Type |
|---|---|
description | string |
quantity | number |
unit_price | decimal |
line_total | decimal |
You get back an array of objects, one per row:
{
"invoice_number": "INV-2026-0417",
"line_items": [
{ "description": "Steel brackets", "quantity": 40, "unit_price": 12.5, "line_total": 500.0 },
{ "description": "Delivery", "quantity": 1, "unit_price": 784.5, "line_total": 784.5 }
]
}
Keys match your column names, values are typed. No cell coordinates, no reconstruction step — iterate the array and insert.
Tip: Give columns the same care as top-level fields. line_total is unambiguous; total in a table sitting on an invoice that also has an overall total is not.
Object fields
An Object field groups related values that appear once. A vendor:
| Child | Type |
|---|---|
name | string |
address | address |
tax_number | string |
{
"vendor": {
"name": "Northgate Supplies",
"address": "12 Mill Lane, Leeds LS1 4AB",
"tax_number": "GB123456789"
}
}
Objects are organisational. vendor.name and vendor_name extract equally well — the object version is easier to read and maps more cleanly onto a nested record downstream.
List fields
A List field holds several simple values with no internal structure — a set of reference numbers, a list of tags.
{ "reference_numbers": ["REF-001", "REF-002", "REF-003"] }
If each entry has more than one property, it is a table, not a list.
Choosing between them
| Your data | Use |
|---|---|
| Appears once, single value | A plain field |
| Appears once, several related values | Object field |
| Repeats, one value each | List field |
| Repeats, several values each | Table field |
The test: does it repeat, and does each entry have more than one property? Repeating with multiple properties is always a table.
Tables inside tables
Nesting a table within a table row is possible but rarely a good idea. It is hard to describe unambiguously, harder to consume, and usually a sign the document should be modelled as two parsers.
If you find yourself reaching for it, consider whether the inner detail actually needs extracting, or whether one flattened table with an extra column expresses the same thing.
What you get when nothing is found
An empty table comes back as an empty array, not null and not a missing key:
{ "line_items": [] }
So iterating is always safe. A null on a column inside a row means that specific cell could not be grounded in the document — the row exists, one value in it did not.
Pages, not rows
A table with two hundred rows costs the same as one with two. Billing counts pages, never rows, fields, or columns. Extract the whole table.
Something here wrong or missing? Tell us — we treat it as a bug.