Merge-field discovery
Before you submit data to render a template, the API can tell you exactly which fields the template expects. Walk the AST, infer the type from usage, return a schema. GET /v1/templates/{id}/fields is the endpoint.
How it works
The template body is parsed into a Twig AST. The discovery walker scans every NameExpression and GetAttrExpression node, builds a tree of {name, type, children, item_type} nodes, and emits the result. Types come from usage:
{{ client_name }}→client_nameisscalar.{{ user.name }}, {{ user.email }}→userisobjectwith childrennameandemail.{% for item in items %}…{% endfor %}→itemsisarraywithitem_type: scalar.{% for line in lines %}{{ line.amount }}{% endfor %}→linesisarraywithitem_type: object, childamount.
Example
Given this template:
<h1>Invoice {{ invoice.number }}</h1>
<p>For {{ client.name }} ({{ client.email }})</p>
{% for line in lines %}
<tr>
<td>{{ line.description }}</td>
<td>{{ line.amount|number_format(2) }}</td>
</tr>
{% endfor %}
<p>Total: {{ totals.subtotal }} + {{ totals.tax }}</p>GET /v1/templates/{id}/fields returns:
{
"fields": [
{
"name": "invoice",
"type": "object",
"required": true,
"children": [
{ "name": "number", "type": "scalar", "required": true }
]
},
{
"name": "client",
"type": "object",
"required": true,
"children": [
{ "name": "name", "type": "scalar", "required": true },
{ "name": "email", "type": "scalar", "required": true }
]
},
{
"name": "lines",
"type": "array",
"required": true,
"item_type": "object",
"children": [
{ "name": "description", "type": "scalar", "required": true },
{ "name": "amount", "type": "scalar", "required": true }
]
},
{
"name": "totals",
"type": "object",
"required": true,
"children": [
{ "name": "subtotal", "type": "scalar", "required": true },
{ "name": "tax", "type": "scalar", "required": true }
]
}
]
}What gets excluded
- Loop variables. In
{% for line in lines %},lineis a local, not a merge field — onlylinesis reported. {% set %}assignments.{% set total = a + b %}makestotallocal; onlyaandbare merge fields.- Twig internals.
loop.index,loop.first,_key,_contextare pseudo-variables — never reported.
Required flag
Every discovered field is required: true in v0.5 — referencing a variable in a template implies the renderer needs it. Optional merge fields with default values are a v2 feature; until then, use Twig's default filter inside the template if you want graceful handling of missing data:
{{ optional_note|default("No note provided.") }}Validation at render time
When you submit a render, the API validates your data against the version's frozen schema. Missing required fields return 422 Unprocessable Entity with a per-field error:
{
"type": "about:blank",
"title": "Validation failed",
"status": 422,
"detail": "Input data is missing required merge fields.",
"errors": {
"data": ["Missing field: client.name"]
}
}Next
Frozen versions — why the schema is captured at freeze time, not render time.