Expressions
A small, safe expression language. It cannot call Java, access the file system, loop forever or allocate unbounded memory.
Where expressions appear
- Bare expression fields:
visibleIf,repeat.over,rows.over,source.binding. - Text templates:
content,cell,header,footer,value,tocTitle. Everything outside{{ … }}is literal text.\nin JSON is a line break.{{{{produces{{.
Values
null, booleans, numbers (decimal, BigDecimal internally, never double for money),
strings (single or double quotes), lists, objects. Dates are ISO 8601 strings in data
("2026-10-11", "2026-10-11T14:30:00+02:00") and are parsed by date filters.
Paths and scope
order.customer.name,order.lines[0].price,order['odd key'].- Missing keys and out-of-range indexes give
null, never an error. Strict mode (render optionstrict: true) turns unknown paths into errors with the data path. - Scope: the render data is the root.
repeat/rowsadd the item under itsasname. Inner scopes shadow outer ones.
Built-in variables
| Variable | Value |
|---|---|
@page |
Page number in the whole document (counted pages only) |
@pageCount |
Total counted pages |
@partPage, @partPageCount |
Same, within the current part |
@index |
0-based index in the innermost repeat |
@first, @last |
Booleans for the innermost repeat |
@renderDate |
From render options; defaults to the date of the request in Europe/Oslo |
@page and @pageCount are only allowed in page templates, PDF part overlays and toc.
Using them in a section is a validation error (page_variable_in_flow), because the value
would change the layout that decides it.
Operators (lowest to highest precedence)
| (filter) · ?? · || · && · == != · < <= > >= · + - · * / % · unary ! - · member/index.
The filter pipe binds loosest, so it applies to the whole expression on its left:
line.qty * line.price | currency('NOK') formats the product. Use parentheses to filter a part.
+ on two strings concatenates. Comparisons between different types are false (no coercion).
Division by zero gives null. Arithmetic with a null operand gives null; with other wrong
types it gives null (strict mode: type_mismatch).
Truthiness (visibleIf, !, &&, ||, if): null, false, 0, "" and the empty
list are falsy; everything else is truthy. && and || return one of their operands.
Inside {{ }} values become text as follows: null → empty, numbers → plain decimal
(2.50 → 2.5), booleans → true/false, lists → items joined with , .
Filters
value | name(args). Filters chain left to right. Every filter except default returns null
for a null input, so x | currency('NOK') | default('-') works.
| Filter | Example | Result |
|---|---|---|
currency(code) |
1234.5 | currency('NOK') |
1 234,50 kr (CLDR format of the template locale) |
number(decimals) |
3 | number(2) |
3,00 |
percent(decimals) |
0.25 | percent(0) |
25 % |
date(pattern) |
'2026-10-11' | date('d. MMMM yyyy') |
11. oktober 2026 |
upper, lower, trim |
||
default(value) |
x | default('-') |
- when x is null or empty string |
truncate(n) |
'abcdef' | truncate(4) |
abc… (at most n characters including …) |
join(sep) |
tags | join(', ') |
|
round(n) |
half-up rounding, returns a number |
Number, currency and percent formats follow CLDR for the template locale and round half-up.
Patterns use java.time format letters. Locale comes from the template's locale.
Functions
sum(list, 'expr'), count(list), min(list, 'expr'), max(list, 'expr'),
filter(list, 'expr'), first(list), last(list), len(stringOrList), if(cond, a, b).
The second argument of list functions is an expression evaluated per item with the item's
fields in scope: sum(order.lines, 'qty * price').
Limits
Expression length ≤ 2000 chars; evaluation step budget 10 000 per expression; list functions over at most 100 000 items. Exceeding a limit is an error with the template path.
Errors
Parse errors are reported at template validation (expression_syntax, with template path and
column). Runtime errors in strict mode (unknown_path, type_mismatch) carry the data path.