Template JSON schema
The shape of a definition
A template definition is one object with two parts: page, which decides the sheet, and elements, the ordered list that is drawn onto it.
{
"page": {
"size": "A4",
"orientation": "portrait",
"margin": { "top": 48, "right": 48, "bottom": 48, "left": 48 }
},
"elements": [
{
"id": "invoice-title",
"type": "heading",
"placementMode": "flow",
"content": { "text": "Invoice" },
"layout": { "width": 100, "widthUnit": "percent", "height": 40 },
"style": { "fontSize": 24, "textStyle": "bold" }
}
]
}
Page sizes: A4 · A3 · Letter. Orientation is portrait or landscape, and margins are in CSS pixels at 96 per inch.
Which element accepts which field
Every element carries id, type, placementMode. Everything else depends on the type, and the table below is generated from the same rule set the validator uses - it cannot fall behind the product.
| type | Fields it accepts |
|---|---|
chart |
band · binding · chart · content · dataRules · layout · style |
container |
band · container · dataRules · layout |
dataset |
band · binding · dataRules · layout · style |
footer |
band · binding · content · dataRules · layout · style |
header |
band · binding · content · dataRules · layout · style |
heading |
band · binding · content · dataRules · layout · style |
image |
band · binding · content · dataRules · layout · style |
pagebreak |
band · dataRules |
pagenumber |
band · content · dataRules · layout · style |
table |
band · binding · columns · content · dataRules · layout · style · summaryRows |
text |
band · binding · content · dataRules · layout · style |
A field that is not in the row for its type fails validation rather than being ignored. A silently dropped field is worse: the document comes out missing something and the request looked successful.
placementMode
How an element finds its position on the sheet. The mode also decides whether layout.x and layout.y are read at all.
flow— stacked one after another down the page; x and y are ignored and the element grows with its contentfree— placed at layout.x and layout.y, exactly where you put itband— repeated on every page in the band named by the element, such as a header or footer
The dataset element
A dataset prints a collection that is not a table: a list of visits, photographs or signatures, drawn with the layout the template gives it rather than in columns.
{
"id": "maintenance-visits",
"type": "dataset",
"placementMode": "flow",
"binding": "visits",
"layout": { "width": 100, "widthUnit": "percent", "height": 160 },
"style": { "fontSize": 11 },
"dataRules": { "required": true }
}
binding names the array in your data, exactly as it does on a table. What the element draws for each item comes from the layout and style, so the same collection can be a table in one template and a dataset in another.
A dataset has no content field. It is fed only by binding - the text that used to sit in content never reached the output, so the field was removed rather than left as a place where writing has no effect.
Checking your data before you render
The validation endpoint answers the question a render would answer, without spending quota: does this data satisfy what the template requires?
curl -X POST https://api.pdfgine.com/api/v1/templates/invoice/validate-data \
-H "X-PDFgine-Key: $PDFGINE_KEY" \
-H "Content-Type: application/json" \
-d '{ "data": { "referenceNo": "DOC-2026-0042", "currency": "USD" } }'
What usually goes wrong
- A field that does not belong to its element type fails validation instead of being ignored. That is deliberate: a silently dropped field produces a document missing something, and nobody looks for what is not there.
- In flow placement layout.x and layout.y are not read at all. Setting them and wondering why nothing moved is the most common schema question.
- A dataset has no content field; it is fed only by binding. Text put in content never reaches the output.
- Margins and sizes are CSS pixels at 96 per inch, not millimetres. A 20 written as millimetres comes out three times too small.
- Rendering to find out whether the data fits costs a page. The validation endpoint answers the same question and costs nothing.