Parsli documentation

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:

text
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:

ColumnType
descriptionstring
quantitynumber
unit_pricedecimal
line_totaldecimal

You get back an array of objects, one per row:

json
{
  "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:

ChildType
namestring
addressaddress
tax_numberstring
json
{
  "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.

json
{ "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 dataUse
Appears once, single valueA plain field
Appears once, several related valuesObject field
Repeats, one value eachList field
Repeats, several values eachTable 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:

json
{ "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.