Template format (v2)
A template is a single JSON object, the ReportDefinition. The format is
snake_case and versioned; the only supported version is 2. Unknown keys are
ignored on input, and new keys are additive and optional so older templates
keep loading.
Coordinates and sizes are millimetres unless the key ends in _pt (points).
The origin is the top-left of the page, with y growing downwards. The
renderer uses the page's top origin, not PDF's bottom-left origin.
Complete example
This is the shape of a full document. The checked-in neutral fixture
crates/jsonpress-core/testdata/invoice.json
is a smaller working version of the same idea.
{
"version": 2,
"name": "Invoice",
"source": "invoice.json",
"font_family": "Helvetica",
"fields": [
{ "name": "CUSTOMER", "label": "Customer", "sample": "Acme Ltd" },
{ "name": "AMOUNT", "label": "Amount", "sample": "1250.00" }
],
"sample_rows": [
{ "CUSTOMER": "Acme Ltd", "AMOUNT": "1250.00" },
{ "CUSTOMER": "Beta Trading", "AMOUNT": "89.50" }
],
"parameters": [
{
"name": "month",
"label": "Month",
"kind": "integer",
"default": 11,
"options": []
}
],
"page": { "width_mm": 210, "height_mm": 297, "bottom_margin_mm": 15 },
"title": [
{
"kind": "text", "x_mm": 20, "y_mm": 15, "width_mm": 170,
"size_pt": 16, "bold": true, "align": "left", "color": "title",
"expr": "\"Invoice\""
},
{
"kind": "text", "x_mm": 20, "y_mm": 40, "width_mm": 90,
"size_pt": 9, "expr": "\"CUSTOMER\""
},
{
"kind": "text", "x_mm": 120, "y_mm": 40, "width_mm": 40,
"size_pt": 9, "align": "right", "expr": "\"AMOUNT\""
}
],
"detail": {
"row_height_mm": 6,
"first_row_y_mm": 50,
"columns": [
{ "x_mm": 20, "width_mm": 90, "size_pt": 9, "expr": "CUSTOMER" },
{
"x_mm": 120, "width_mm": 40, "size_pt": 9,
"align": "right", "expr": "AMOUNT"
}
]
},
"note": {
"x_mm": 20, "width_mm": 170, "size_pt": 8,
"offset_mm": 5, "extra_mm": 4, "expr": "\"Thank you\""
},
"summary": [
{
"kind": "text", "x_mm": 20, "y_mm": 10, "width_mm": 90,
"size_pt": 9, "bold": true, "expr": "\"Total\""
},
{
"kind": "text", "x_mm": 120, "y_mm": 10, "width_mm": 40,
"size_pt": 9, "bold": true, "align": "right",
"expr": "AMOUNT", "total_type": 2
}
]
}
Top-level keys
| Key | Type | Required | Meaning |
|---|---|---|---|
version | integer | yes | must be 2 |
name | string | yes | template name; PDF document title and download file stem |
source | string | no | optional audit trail (original file name), default "" |
font_family | string or null | no | PDF font family from the server catalog; DOCX names the family for Word; unset uses the default |
page | object | yes | width_mm, height_mm, bottom_margin_mm |
title | array of elements | yes | printed on every page |
detail | object | yes | the repeating row band |
note | object | no | optional line below each detail row |
summary | array of elements | yes (may be []) | printed after the last detail row |
fields | array | no | designer field catalog |
sample_rows | array of objects | no | designer preview rows |
parameters | array | no | designer parameter form |
Earlier templates may contain font_family as metadata even though it was
previously ignored. Remove that property to use the server default, or configure
the named family in JSONPRESS_FONTS before rendering the template as PDF.
Text elements and detail columns may override the report's font_family.
An omitted, null, empty or whitespace-only override inherits the report font;
an unset or blank report font uses the server default (Helvetica if none is
configured). Names are trimmed. PDF requires each selected family to be in the
server's font catalog and fails for unknown families. DOCX names the selected
family for Word; XLSX and CSV are unaffected. Table cells and note lines use
the report font and do not have per-object overrides.
PDF measures text using the selected family and regular or bold face for wrapping, clipping and alignment. Text that does not fit its field is truncated with an ellipsis when the face contains that glyph and it fits; otherwise it is truncated without one. A stretched column wraps at word boundaries, splitting long words (including unspaced Chinese text) between characters. A field too narrow for even one glyph cannot display that glyph.
fields, sample_rows, parameters
These three keys are additive v2 metadata. The render path ignores all three; the designer uses them to populate its field rail, sample data and parameter form.
"fields": [
{ "name": "CUSTOMER", "label": "Customer", "sample": "Acme Ltd" }
],
"sample_rows": [
{ "CUSTOMER": "Acme Ltd" }
],
"parameters": [
{ "name": "month", "label": "Month", "kind": "integer",
"default": 11, "options": [] }
]
fieldsis the designer's catalog.nameis the row key,labelis the human label andsampleis a placeholder value (default""). If absent, the designer infers fields from expressions and sample rows.sample_rowsis design-time data only. The designer previews the first 50 rows and writes the parsed rows to this key; a render never uses it:POST /v1/rendersalways usesdata.rowsfrom the request. The key issample_rowswith a snake_case underscore —sampleRowsis a different, unknown key and has no effect.parametersdeclares the typed parameter form.kindistext,integer,booleanorselect;optionsare{ "value": "...", "label": "..." }pairs for a select.defaultis the initial value the designer shows for that kind. This is design-time metadata, distinct from the request'sparamsstring map.
Page geometry
"page": {
"width_mm": 210, "height_mm": 297,
"top_margin_mm": 0, "right_margin_mm": 0, "bottom_margin_mm": 15, "left_margin_mm": 0
}
bottom_margin_mm is the bottom print limit. The flow engine stops adding
rows past height_mm - bottom_margin_mm. A4 portrait is 210 × 297; A4
landscape is 297 × 210.
top_margin_mm, left_margin_mm and right_margin_mm are optional and
default to 0. They define a print area for validation: when a side margin is
set, an element whose box crosses it fails the render with an error naming the
band and index, for example title[3] ends at 290.0 mm, right of the right margin (287.0 mm). When any of these margins is nonzero, vertical positions
of title objects, rendered detail and note lines, and paginated summaries are
also checked against the top and bottom limits. Text bounds use its baseline
and font size. Margins never move, clip or rescale objects; templates with
all three new margins zero keep their previous behavior.
Band model
A report is four vertical bands:
- Title — drawn at the top of every page. It usually holds the report title, column headers and decorative lines/rectangles.
- Detail — repeats one row per data row.
first_row_y_mmis the first row's baseline;row_height_mmis the band height and the pitch between rows. When a page fills, the flow continues on the next page. Optionaldetail.elementsrepeats text, lines, rectangles, tables and images once per row; theiry_mmvalues are relative to the row's first baseline. - Note — optional. When the note expression is non-empty, a line is
drawn below the detail row;
extra_mmis the extra flow height it consumes. - Summary — drawn after the last detail row, on the same page when it
fits and otherwise on a new one. Element
y_mmvalues are relative to the top of the summary block, not to the page.
Pagination is planned before drawing, because stretched detail cells and note lines change row heights.
Elements
Every element is tagged by kind.
Text
{
"kind": "text", "x_mm": 20, "y_mm": 40, "width_mm": 90,
"size_pt": 9, "bold": false, "align": "left", "color": "default",
"expr": "CUSTOMER"
}
| Key | Required | Default | Meaning |
|---|---|---|---|
x_mm, y_mm, width_mm, size_pt | yes | — | position and box width; y_mm is the baseline |
expr | yes | — | expression, see expressions.md |
bold | no | false | bold weight |
align | no | left | left or right |
color | no | default | default, title or muted |
font_family | no | report font | server-catalog family override for PDF; family name for DOCX |
total_type | no | 0 | in a summary band: 0 plain, 1 count, 2 sum |
A quoted literal such as "\"Invoice\"" prints fixed text; a bare identifier
such as "CUSTOMER" reads the row field.
Line
{
"kind": "line", "x0_mm": 20, "y0_mm": 38,
"x1_mm": 190, "y1_mm": 38, "thickness_mm": 0.2
}
All keys are required.
Rectangle
{
"kind": "rect", "x0_mm": 20, "y0_mm": 12,
"width_mm": 170, "height_mm": 10, "filled": false
}
x0_mm, y0_mm, width_mm and height_mm are required; filled defaults to
false.
Image
{
"kind": "image", "x0_mm": 20, "y0_mm": 12,
"width_mm": 40, "height_mm": 16,
"src": "data:image/png;base64,iVBORw0KGgo..."
}
An image is a fixed PNG or JPEG embedded in the template, useful for a logo or
letterhead. src must be a base64 data:image/png or data:image/jpeg URL,
so a template has no external references and renders offline. All keys are
required. In the designer, choose Image in the toolbox and pick a file.
PDF and DOCX scale images to width_mm × height_mm; XLSX and CSV omit images.
Each encoded image is limited to 8 MiB; unique images in one report may total
at most 8 million decoded pixels. Images follow element order in each band.
Table
{
"kind": "table", "x0_mm": 20, "y0_mm": 15,
"width_mm": 100, "height_mm": 20, "size_pt": 9,
"border": "solid",
"cells": [
["\"Item\"", { "expr": "\"Amount\"", "align": "right" }],
["\"Example\"", { "expr": "report_total", "align": "right" }]
]
}
Tables are fixed grids in the title or summary band. Columns and rows have equal
sizes by default. Optional column_weights and row_weights arrays set their
relative sizes; each must contain one positive number per column or row. Sizes
are proportional to the weights and the table's total width or height. In XLSX,
column widths apply to the entire worksheet, including other tables and detail rows;
the last table with custom column widths determines the shared widths.
Each cell contains an expression; quote literal text as shown. Cells use the
same parameter and page context as other text in their band. Rows and columns
must be rectangular and nonempty. Title tables repeat on each page; summary
tables appear in the summary. XLSX exports each table as spreadsheet cells;
CSV omits tables and the HTTP response includes an X-JSONPress-Warning header.
The optional border is solid (default), dashed, dotted, or none in
PDF and XLSX. A cell is either an expression string, left-aligned, or an object
{ "expr": "...", "align": "left" | "center" | "right" }; alignment applies to
PDF and XLSX. In PDF, cell text is fitted and aligned using the configured
font's glyph advances, including the width of spaces. Ordinary text and detail
columns retain their existing left/right alignment options.
Detail columns
{
"x_mm": 20, "width_mm": 90, "size_pt": 9,
"bold": false, "align": "left", "stretch": false,
"label": "Customer", "expr": "CUSTOMER"
}
x_mm, width_mm, size_pt and expr are required.
font_familyoptionally overrides the report font for this column in PDF and DOCX; omit it to inherit the report font.stretch: truewraps a value that is too wide instead of clipping it; wrapped rows consume extra vertical flow.labelis optional. The server fills it from the nearest literal header in the title band (the default-colour text whosex_mmis within 6 mm of the column) so PDF and XLSX/CSV headings agree. An explicitoptions.headersin the request overrides it for XLSX/CSV.
Detail objects
detail.elements is an optional array of the same element objects used by the
title and summary bands — text, line, rect, table and image. Each object is
drawn once per data row, with y_mm measured from that row's first baseline.
Expressions use the row's values and the current page. An object that extends
below row_height_mm increases that row's height and therefore pagination.
XLSX and CSV omit detail objects.
DOCX includes detail objects in an editable flow layout; positions are
approximated and Word determines pagination.
In DOCX, rectangles and non-horizontal lines become editable Word shapes.
Horizontal positions are retained where the flow layout permits; overlapping
detail columns may be narrowed. Word determines vertical placement.
Notes
{
"x_mm": 20, "width_mm": 170, "size_pt": 8,
"offset_mm": 5, "extra_mm": 4, "expr": "NOTE"
}
All keys are required. The note prints only when the expression is non-empty
after trimming. offset_mm is the distance from the row baseline to the note
baseline; extra_mm is the extra flow height consumed. In XLSX/CSV the note is
added as a final Notes column when options.includeNotes is true.
Rows
Rows are supplied in the request, not in the template:
{ "data": { "rows": [ { "customer": "Acme", "amount": 1250, "active": true } ] } }
Keys are matched case-insensitively and normalised to uppercase. Values may be
string, number, boolean or null; objects and arrays are rejected. See
api.md for the full conversion and sparse-row rules.