DocgenTry it

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_name is scalar.
  • {{ user.name }}, {{ user.email }}user is object with children name and email.
  • {% for item in items %}…{% endfor %}items is array with item_type: scalar.
  • {% for line in lines %}{{ line.amount }}{% endfor %}lines is array with item_type: object, child amount.

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 %}, line is a local, not a merge field — only lines is reported.
  • {% set %} assignments. {% set total = a + b %} makes total local; only a and b are merge fields.
  • Twig internals. loop.index, loop.first, _key, _context are 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.