Expression Utility Objects
|
This content was generated with the assistance of AI and should be verified against the official documentation before being relied on in production. The reference material consulted while preparing these pages is listed in the section bibliography. |
This page is written against Thymeleaf 3.1.5.RELEASE (2026-04-21), Spring Boot 4.1.1 (Spring Framework 7.0.9) and Java 21, checked on
2026-10-08, and every example was compiled and run on that stack (Maven wrapper, MockMvc against the Bookshelf scratch application, JVM
time zone pinned to UTC, request locales en-US and es-ES). Everything on it applies to the Standard Dialect, so it works in all
three environments: standalone (plain TemplateEngine), Spring MVC and Spring WebFlux. The few objects that belong to the
Spring integration (#fields, #mvc, #themes) and to the Spring Security extras (#authentication, #authorization)
are called out in their own section, because they only exist when the corresponding dialect is registered. Layout Dialect, htmx and
the other add-ons are not involved.
What utility objects are
An expression object is a name that starts with # and is available inside any variable expression, in addition to the model
variables you put in the context. Thymeleaf splits them in two groups. The basic objects (#ctx, which exposes the locale, the variable names and, in a web environment, the web exchange, and
#locale) expose the evaluation context itself and are listed on the Standard Expression Syntax page. The utility objects, which this page covers, are stateless helper
objects with a method per task: formatting a date, padding a number, testing a string, summing a list. They are the template-side
equivalent of a small standard library, and they exist so that presentation logic such as "show the date in the reader’s locale" does
not have to be computed in the controller and stored in the model.
Four properties are worth knowing before the method tables:
-
They are plain method calls on a named object, so
${#strings.toUpperCase(name)}is an ordinary variable expression. Used inside*{...},#{...}arguments,@{...}parameters,th:ifconditions and everywhere else an expression is allowed, they behave the same. -
Most families come in four shapes. A scalar method (
format), anarray*variant, alist*variant and aset*variant that apply the same operation to every element (arrayFormat,listFormat,setFormat). The tables below name the scalar method and say when the other three exist, instead of repeating them. -
They are locale-aware only where the table says so. The locale is taken from the context (the request locale in Spring MVC and WebFlux,
Context.setLocalestandalone), so one template renders differently foren-USandes-ESwithout any change. -
Their outputs are escaped like any other value. The examples use
th:text, so a&in a result is written as&. A helper such as#strings.escapeXmltherefore double-escapes when its result is passed toth:text; see the pitfalls.
The utility objects are implemented by classes in the org.thymeleaf.expression package of thymeleaf core, registered by the
Standard Dialect through its expression object factory. The Javadoc of each class is the authoritative method list, and Appendix B of
the Using Thymeleaf tutorial is the narrative one.
| Object | Handles | Locale-aware |
|---|---|---|
|
Template names and modes being processed |
No |
|
Externalised messages, same lookup as |
Yes |
|
Escaping and unescaping URI parts |
No |
|
The configured conversion service |
Depends on the converter |
|
|
Yes |
|
|
Yes |
|
Formatting and sequences |
Yes |
|
Text |
No |
|
Null safety and truthiness |
No |
|
Collections |
No |
|
Unique |
No |
The scratch application
All outputs below come from one controller that fills the model and one template with a paragraph per example. The domain is the
running Bookshelf scenario of this section, with a few extra values (a legacy java.util.Date, a Calendar, a LocalDate, a
LocalDateTime and a ZonedDateTime) so that every family has something to work on.
@Controller
public class UtilityController {
@GetMapping("/util")
public String demo(Model model) {
model.addAttribute("title", " the dispossessed ");
model.addAttribute("isbn", "978-0-06-051275-4");
model.addAttribute("price", new BigDecimal("1234.5"));
model.addAttribute("rating", 0.875);
model.addAttribute("pages", 7);
model.addAttribute("published", LocalDate.of(1974, 5, 1));
model.addAttribute("loanedAt", LocalDateTime.of(2026, 10, 8, 14, 30, 15));
model.addAttribute("dueAt", ZonedDateTime.of(2026, 10, 22, 9, 0, 0, 0, ZoneId.of("UTC")));
GregorianCalendar cal = new GregorianCalendar(2026, Calendar.OCTOBER, 8, 14, 30, 15);
cal.setTimeZone(TimeZone.getTimeZone("UTC"));
model.addAttribute("calendar", cal);
model.addAttribute("legacyDate", cal.getTime());
model.addAttribute("authors", List.of("Le Guin", "Asimov", "Butler", "Asimov"));
model.addAttribute("tags", new LinkedHashSet<>(List.of("sf", "classic")));
model.addAttribute("stock", new LinkedHashMap<>(Map.of("dune", 3)));
model.addAttribute("pageCounts", new int[] {300, 412, 250});
model.addAttribute("prices", List.of(new BigDecimal("10.00"), new BigDecimal("12.50"), new BigDecimal("7.25")));
model.addAttribute("nothing", null);
model.addAttribute("flags", List.of(true, true, false));
model.addAttribute("unsafeName", "Ursula K. Le Guin/Q&A #1");
model.addAttribute("quoted", "It's a \"book\"");
return "util/demo";
}
}
The message bundle used by the #messages examples has two keys with parameters, one per language:
# messages.properties and messages_en.properties
books.title=Books
books.new=New book
books.count=The shelf holds {0} books by {1}
# messages_es.properties
books.title=Libros
books.new=Nuevo libro
books.count=La estanteria tiene {0} libros de {1}
Two Boot settings keep the outputs reproducible. By default Boot’s message source falls back to the JVM’s own locale when a bundle has
no entry for the requested one; spring.messages.fallback-to-system-locale=false turns that off, so the output depends only on the
request. The JVM default time zone decides how a java.util.Date is printed, so the test pins it to UTC.
Execution information: #execInfo
#execInfo describes the template that is currently being processed. It is the one utility object with state, because the answer
changes as fragments are inserted. It has two views of the same stack: the leaf template (where the events being processed were
parsed) and the root template (the one the engine was originally asked to process).
| Expression | Result |
|---|---|
|
Name and mode of the leaf template |
|
Name and mode of the root template |
|
Stacks (lists), root first and leaf last, with every inserted fragment’s template in between |
|
The same stack as a list of |
<!-- util/demo.html -->
<p id="exec1" th:text="${#execInfo.templateName} + ' / ' + ${#execInfo.templateMode}">x</p>
<div th:insert="~{util/frag :: info}">x</div>
<p id="exec3" th:text="${#execInfo.templateNames}">x</p>
<!-- util/frag.html -->
<p th:fragment="info" id="exec2"
th:text="${#execInfo.templateName} + ' in ' + ${#execInfo.processedTemplateName}">x</p>
<p id="exec1">util/demo / HTML</p>
<div><p id="exec2">util/frag in util/demo</p></div>
<p id="exec3">[util/demo]</p>
Source: Using Thymeleaf — Execution Info
After the fragment has been inserted, the stack is back to the root template alone, which is why the last line prints one name. The typical use is a debugging footer or an HTML comment showing which template produced a block.
#execInfo is one of the objects that restricted expression mode forbids. In a th:utext attribute (and in the other
contexts listed on the Template Security & XSS page) the same expression fails at runtime:
TemplateProcessingException: Access to variable '#execInfo' is forbidden in this context
Messages: #messages
#messages performs the same lookup as the #{...} message expression, but as a method call, so the key can be computed and the
result can be post-processed inside a larger expression.
| Method | Behaviour |
|---|---|
|
The message, or the placeholder |
|
Same, with any number of parameters in an array |
|
Looks up a collection of keys and returns the same kind of collection of messages |
|
The same family, but |
<p id="msg1" th:text="${#messages.msg('books.title')}">x</p>
<p id="msg2" th:text="${#messages.msg('books.count', 3, 'Le Guin')}">x</p>
<p id="msg3" th:text="${#messages.msg('books.unknown')}">x</p>
<p id="msg4" th:text="${#messages.msgOrNull('books.unknown')} ?: 'no such key'">x</p>
<p id="msg5" th:text="${#messages.listMsg({'books.title', 'books.new'})}">x</p>
<p id="msg6" th:text="#{books.count(3, 'Le Guin')}">x</p>
<!-- request locale en-US -->
<p id="msg1">Books</p>
<p id="msg2">The shelf holds 3 books by Le Guin</p>
<p id="msg3">??books.unknown_en_US??</p>
<p id="msg4">no such key</p>
<p id="msg5">[Books, New book]</p>
<p id="msg6">The shelf holds 3 books by Le Guin</p>
<!-- request locale es-ES -->
<p id="msg1">Libros</p>
<p id="msg2">La estanteria tiene 3 libros de Le Guin</p>
<p id="msg3">??books.unknown_es_ES??</p>
<p id="msg4">no such key</p>
<p id="msg5">[Libros, Nuevo libro]</p>
<p id="msg6">La estanteria tiene 3 libros de Le Guin</p>
msgOrNull is the way to give a missing key a real fallback. The #{...} syntax is shorter for a fixed key and is preferred
there; #messages is for the cases where the key itself is an expression or a collection of keys. The Internationalization and
Validation & Error Messages pages go deeper into bundles and the Spring MessageSource.
URIs: #uris
#uris escapes and unescapes text for use in each part of a URI. The part matters because the set of characters that must be
percent-encoded differs between a path, a path segment, a query parameter and a fragment identifier. Every method has an overload that takes the character encoding as a second argument.
| Methods | Escapes for |
|---|---|
|
A whole URI path (the |
|
One segment between slashes (a |
|
The fragment identifier after |
|
A query parameter name or value ( |
<p id="uri1" th:text="${#uris.escapePath(unsafeName)}">x</p>
<p id="uri2" th:text="${#uris.escapePathSegment(unsafeName)}">x</p>
<p id="uri3" th:text="${#uris.escapeQueryParam(unsafeName)}">x</p>
<p id="uri4" th:text="${#uris.escapeFragmentId(unsafeName)}">x</p>
<p id="uri5" th:text="${#uris.unescapeQueryParam('Le%20Guin%26Co')}">x</p>
<a id="uri6" th:href="@{/search(q=${unsafeName})}">x</a>
<p id="uri1">Ursula%20K.%20Le%20Guin/Q&A%20%231</p>
<p id="uri2">Ursula%20K.%20Le%20Guin%2FQ&A%20%231</p>
<p id="uri3">Ursula%20K.%20Le%20Guin/Q%26A%20%231</p>
<p id="uri4">Ursula%20K.%20Le%20Guin/Q&A%20%231</p>
<p id="uri5">Le Guin&Co</p>
<a id="uri6" href="/search?q=Ursula%20K.%20Le%20Guin/Q%26A%20%231">x</a>
Source: Using Thymeleaf — URIs/URLs
Compare the first four lines: only escapePathSegment encodes the slash, and only escapeQueryParam encodes the &. (The & in
the first, second and fourth outputs is the HTML escaping that th:text applies to a literal &; the URI escaper left it alone.)
The last line shows the better habit: a link expression @{/search(q=${unsafeName})} encodes its parameters for you. Use
#uris for the cases a link expression cannot cover, such as building a value for a data-* attribute or an hx-get target
string; never concatenate raw user input into a URL. The link expressions and URL rewriting are covered on the Link URLs page.
Conversions: #conversions
#conversions runs the engine’s conversion service at any point of a template, with one method, convert(object, targetClass).
The target may be a Class or a class name given as a string. In a Spring application the service is the Spring ConversionService
(it is the same one that formats ${{...}}), so every converter and formatter the application registered is available, including the formatters Boot registers for java.time.
<p id="conv1" th:text="${#conversions.convert('42', 'java.lang.Integer') + 1}">x</p>
<p id="conv2" th:text="${#conversions.convert('2026-10-08', 'java.time.LocalDate')}">x</p>
<p id="conv3" th:text="${{published}}">x</p>
<!-- en-US -->
<p id="conv1">43</p>
<p id="conv2">2026-10-08</p>
<p id="conv3">5/1/74</p>
<!-- es-ES -->
<p id="conv3">1/5/74</p>
The first line shows that the result is a real Integer (it can be added to); the third shows the double-brace form for contrast, whose
output follows the request locale. Standalone, without Spring, the service is Thymeleaf’s own standard one, which knows far fewer
conversions; the Conversion & Formatting page compares them.
Dates and calendars: #dates and #calendars
#dates works on java.util.Date and #calendars on java.util.Calendar. The two have identical method names. The legacy
types remain in use in JPA entities, older libraries and java.sql values; for new code the model should carry java.time values
and the next section applies.
| Methods (the four shapes exist for all of them) | Result |
|---|---|
|
Date and time in the standard format of the context locale |
|
Formatted with a |
|
ISO-8601 text |
|
The numeric component |
|
The component as a number or as locale-aware text |
|
A new |
|
The current instant or day |
|
Same creation overloads with a trailing |
<p id="d1" th:text="${#dates.format(legacyDate)}">x</p>
<p id="d2" th:text="${#dates.format(legacyDate, 'dd/MMM/yyyy HH:mm')}">x</p>
<p id="d3" th:text="${#dates.formatISO(legacyDate)}">x</p>
<p id="d4" th:text="${#dates.year(legacyDate)} + ' ' + ${#dates.monthName(legacyDate)} + ' ' + ${#dates.dayOfWeekName(legacyDate)}">x</p>
<p id="d5" th:text="${#dates.format(#dates.create(2026, 12, 25), 'yyyy-MM-dd')}">x</p>
<p id="c1" th:text="${#calendars.format(calendar, 'dd/MMM/yyyy HH:mm')}">x</p>
<p id="c2" th:text="${#calendars.formatISO(calendar)}">x</p>
<p id="c3" th:text="${#calendars.monthName(calendar)} + ' ' + ${#calendars.dayOfWeekNameShort(calendar)}">x</p>
<!-- en-US -->
<p id="d1">October 8, 2026, 2:30:15 PM UTC</p>
<p id="d2">08/Oct/2026 14:30</p>
<p id="d3">2026-10-08T14:30:15.000+00:00</p>
<p id="d4">2026 October Thursday</p>
<p id="d5">2026-12-25</p>
<p id="c1">08/Oct/2026 14:30</p>
<p id="c2">2026-10-08T14:30:15.000+00:00</p>
<p id="c3">October Thu</p>
<!-- es-ES -->
<p id="d1">8 de octubre de 2026, 14:30:15 UTC</p>
<p id="d2">08/oct/2026 14:30</p>
<p id="d4">2026 octubre jueves</p>
<p id="c3">octubre jue</p>
The default format(d) output follows the request locale ("October 8, 2026, …" in English, "8 de octubre de 2026, …" in Spanish). Because the JVM’s default
time zone is used to print a Date, two servers in different zones show different wall-clock text for the same instant; set the zone
explicitly (a TimeZone on the JVM, or the zoned methods of the next section) when the output must be stable.
Temporals: #temporals (java.time)
#temporals is the java.time counterpart of #dates: it formats LocalDate, LocalDateTime, LocalTime, ZonedDateTime,
OffsetDateTime, Instant and the other Temporal types. It is part of thymeleaf core since Thymeleaf 3.1. Before 3.1 it
lived in the separate thymeleaf-extras-java8time module (last release 3.0.4), which is still found in older articles and in the
books; a 3.1 project does not need it and should not declare it.
| Methods (the four shapes exist for the formatters and getters) | Result |
|---|---|
|
The default locale format (a medium style for dates and date-times) |
|
A |
|
The value converted to the given time zone before formatting |
|
ISO-8601 text |
|
The numeric component |
|
Number or locale-aware text |
|
A |
|
|
|
Parse text into a |
<p id="t1" th:text="${#temporals.format(published)}">x</p>
<p id="t2" th:text="${#temporals.format(loanedAt, 'dd MMMM yyyy HH:mm')}">x</p>
<p id="t3" th:text="${#temporals.format(loanedAt, 'FULL')}">x</p>
<p id="t5" th:text="${#temporals.format(dueAt, 'dd/MM/yyyy HH:mm', 'Europe/Paris')}">x</p>
<p id="t6" th:text="${#temporals.dayOfWeekName(published)} + ' ' + ${#temporals.monthNameShort(published)} + ' ' + ${#temporals.year(published)}">x</p>
<p id="t7" th:text="${#temporals.create(2026, 12, 25)}">x</p>
<p id="t8" th:text="${#temporals.createDate('2026-10-08')}">x</p>
<p id="t9" th:text="${#temporals.createDate('08/10/2026', 'dd/MM/yyyy').dayOfYear}">x</p>
<p id="t10" th:text="${#temporals.format(loanedAt, 'EEEE d MMMM', #locale)}">x</p>
<!-- en-US -->
<p id="t1">May 1, 1974</p>
<p id="t2">08 October 2026 14:30</p>
<p id="t3">Thursday, October 8, 2026, 2:30:15 PM Coordinated Universal Time</p>
<p id="t5">22/10/2026 11:00</p>
<p id="t6">Wednesday May 1974</p>
<p id="t7">2026-12-25</p>
<p id="t8">2026-10-08</p>
<p id="t9">281</p>
<p id="t10">Thursday 8 October</p>
<!-- es-ES -->
<p id="t1">1 de mayo de 1974</p>
<p id="t2">08 octubre 2026 14:30</p>
<p id="t6">miércoles may 1974</p>
<p id="t10">jueves 8 octubre</p>
Line t5 shows the zone conversion (09:00 UTC is 11:00 in Paris on 22 October, a summer-time date), and line t10 shows how to
format with the locale of the current request by passing the basic object #locale explicitly: the same template yields
"Thursday 8 October" and "jueves 8 octubre". That is what a weekly calendar view needs, where a column header is the weekday name in the reader’s language.
The pattern letters are those of java.time.format.DateTimeFormatter, which are not identical to the SimpleDateFormat ones used by
#dates for the less common letters, so check the Javadoc of the formatter when you copy a pattern between the two.
Which family to reach for depends on the type in the model:
Numbers: #numbers
#numbers formats integers, decimals, currency and percentages with the context locale, and generates integer sequences. The
formatting methods all have the four shapes (formatInteger, arrayFormatInteger, listFormatInteger, setFormatInteger).
| Method | Result |
|---|---|
|
Integer, padded with zeros to a minimum number of digits |
|
Decimal with an exact number of fraction digits |
|
Same, choosing separators |
|
Locale currency format |
|
Percentage; the input is the fraction (0.875 is 87.5%) |
|
An |
The separator names accepted by the formatters are POINT, COMMA, WHITESPACE, NONE and DEFAULT (take the locale’s own).
<p id="n1" th:text="${#numbers.formatInteger(pages, 3)}">x</p>
<p id="n2" th:text="${#numbers.formatInteger(1234567, 1, 'POINT')}">x</p>
<p id="n3" th:text="${#numbers.formatDecimal(price, 1, 2)}">x</p>
<p id="n4" th:text="${#numbers.formatDecimal(price, 1, 'WHITESPACE', 2, 'COMMA')}">x</p>
<p id="n5" th:text="${#numbers.formatCurrency(price)}">x</p>
<p id="n6" th:text="${#numbers.formatPercent(rating, 1, 1)}">x</p>
<p id="n7" th:text="${#numbers.listFormatDecimal(prices, 2, 2)}">x</p>
<ul id="n8"><li th:each="i : ${#numbers.sequence(1, 5)}" th:text="${i}">x</li></ul>
<p id="n9" th:text="${#strings.arrayJoin(#numbers.sequence(0, 10, 5), ',')}">x</p>
<!-- en-US -->
<p id="n1">007</p>
<p id="n2">1.234.567</p>
<p id="n3">1234.50</p>
<p id="n4">1 234,50</p>
<p id="n5">$1,234.50</p>
<p id="n6">87.5%</p>
<p id="n7">[10.00, 12.50, 07.25]</p>
<ul id="n8"><li>1</li><li>2</li><li>3</li><li>4</li><li>5</li></ul>
<p id="n9">0,5,10</p>
<!-- es-ES -->
<p id="n3">1234,50</p>
<p id="n5">1.234,50 €</p>
<p id="n6">87,5 %</p>
<p id="n7">[10,00, 12,50, 07,25]</p>
Source: Using Thymeleaf — Numbers
Note the third line: formatDecimal(price, 1, 2) uses no thousands separator, so 1234.50; asking for one is what the
five-argument form on line n4 does. The sequence on line n8 is the idiomatic way to render a pagination bar or a star rating
without a model list. When the model value is a BigDecimal money amount, keep the arithmetic in Java and use #numbers only to
present it.
Strings: #strings
#strings is the largest family. Almost every method also exists with the array, list and set prefixes, in which case it
maps over the collection and returns a collection.
| Group | Methods |
|---|---|
Conversion and emptiness |
|
Searching and comparing |
|
Extracting |
|
Rewriting |
|
Case |
|
Joining and splitting |
|
Escaping |
|
Random |
|
<p id="s1" th:text="'[' + ${#strings.trim(title)} + ']'">x</p>
<p id="s2" th:text="${#strings.capitalizeWords(#strings.trim(title))}">x</p>
<p id="s3" th:text="${#strings.abbreviate(#strings.trim(title), 10)}">x</p>
<p id="s4" th:text="${#strings.substringAfter(isbn, '978-')} + ' ' + ${#strings.length(isbn)}">x</p>
<p id="s5" th:text="${#strings.listJoin(authors, ', ')}">x</p>
<p id="s6" th:text="${#strings.listSplit('a,b,c', ',')}">x</p>
<p id="s7" th:text="${#strings.isEmpty(nothing)} + ' ' + ${#strings.defaultString(nothing, 'n/a')}">x</p>
<p id="s8" th:text="${#strings.replace(isbn, '-', '')} + ' ' + ${#strings.toUpperCase(#strings.trim(title))}">x</p>
<p id="s9" th:text="${#strings.concat('Vol. ', pages)} + ' ' + ${#strings.concatReplaceNulls('-', 'a', nothing, 'c')}">x</p>
<p id="s10" th:text="${#strings.contains(title, 'dispossessed')} + ' ' + ${#strings.startsWith(#strings.trim(title), 'the')} + ' ' + ${#strings.equalsIgnoreCase('Dune', 'DUNE')}">x</p>
<p id="s11" th:text="${#strings.escapeJavaScript(quoted)}">x</p>
<p id="s12" th:text="${#strings.escapeXml('<b>&</b>')}">x</p>
<p id="s13" th:text="${#strings.length(#strings.randomAlphanumeric(8))}">x</p>
<p id="s1">[the dispossessed]</p>
<p id="s2">The Dispossessed</p>
<p id="s3">the dis...</p>
<p id="s4">0-06-051275-4 17</p>
<p id="s5">Le Guin, Asimov, Butler, Asimov</p>
<p id="s6">[a, b, c]</p>
<p id="s7">true n/a</p>
<p id="s8">9780060512754 THE DISPOSSESSED</p>
<p id="s9">Vol. 7 a-c</p>
<p id="s10">true true true</p>
<p id="s11">It\'s a \"book\"</p>
<p id="s12">&lt;b&gt;&amp;&lt;/b&gt;</p>
<p id="s13">8</p>
Source: Using Thymeleaf — Strings
Three details deserve attention. abbreviate(s, 10) counts the three dots inside the limit. concatReplaceNulls is the variant that substitutes a visible replacement for null values (in the scratch run a plain concat('a', null) produced just a, so the explicit version is the one to use when a missing value must show as a marker). And lines s11 and s12 show the double escaping: th:text already escapes its result, so escapeXml in front of it
turns < into <. The escaping helpers are for the rare case where the target is not an HTML text node, such as building
a string for an attribute that th:attr will write without further processing; for JavaScript values the safer tool is JavaScript
inlining, described on the Inlining page.
Objects and booleans: #objects and #bools
#objects has one job, null safety: nullSafe(obj, default) returns the default when the object is null, with
arrayNullSafe, listNullSafe and setNullSafe to replace the null elements of a collection. In an expression the Elvis
operator ?: often reads better for a single value; nullSafe earns its place in the collection variants.
#bools evaluates values as booleans (it accepts, for example, the string "true") and adds aggregate checks:
| Methods | Result |
|---|---|
|
Whether the value evaluates to true or to false |
|
|
|
|
<p id="o1" th:text="${#objects.nullSafe(nothing, 'fallback')}">x</p>
<p id="o2" th:text="${#objects.listNullSafe({'a', null}, 'z')}">x</p>
<p id="b1" th:text="${#bools.isTrue('true')} + ' ' + ${#bools.isFalse(false)}">x</p>
<p id="b2" th:text="${#bools.listAnd(flags)} + ' ' + ${#bools.listOr(flags)}">x</p>
<p id="o1">fallback</p>
<p id="o2">[a, z]</p>
<p id="b1">true true</p>
<p id="b2">false true</p>
Collections: #arrays, #lists, #sets, #maps and #aggregates
The collection objects are intentionally small: size, emptiness, membership, conversion and, for lists, sorting. Anything richer (filtering, grouping, mapping) belongs in the controller or in SpEL’s own projection and selection operators when Spring EL is the expression language.
| Object | Methods |
|---|---|
|
|
|
|
|
|
|
|
|
|
<p id="a1" th:text="${#arrays.length(pageCounts)} + ' ' + ${#arrays.contains(pageCounts, 412)} + ' ' + ${#arrays.isEmpty(pageCounts)}">x</p>
<p id="a2" th:text="${#strings.arrayJoin(#arrays.toStringArray(authors), '|')}">x</p>
<p id="l1" th:text="${#lists.size(authors)} + ' ' + ${#lists.contains(authors, 'Butler')} + ' ' + ${#lists.isEmpty(authors)}">x</p>
<p id="l2" th:text="${#lists.sort(authors)}">x</p>
<p id="l3" th:text="${#lists.toList(tags)}">x</p>
<p id="se1" th:text="${#sets.size(tags)} + ' ' + ${#sets.contains(tags, 'sf')} + ' ' + ${#sets.toSet(authors)}">x</p>
<p id="m1" th:text="${#maps.size(stock)} + ' ' + ${#maps.containsKey(stock, 'dune')} + ' ' + ${#maps.containsValue(stock, 3)} + ' ' + ${#maps.isEmpty(stock)}">x</p>
<p id="ag1" th:text="${#aggregates.sum(prices)} + ' ' + ${#aggregates.avg(prices)}">x</p>
<p id="ag2" th:text="${#aggregates.sum(pageCounts)} + ' ' + ${#aggregates.avg(pageCounts)}">x</p>
<p id="a1">3 true false</p>
<p id="a2">Le Guin|Asimov|Butler|Asimov</p>
<p id="l1">4 true false</p>
<p id="l2">[Asimov, Asimov, Butler, Le Guin]</p>
<p id="l3">[sf, classic]</p>
<p id="se1">2 true [Le Guin, Asimov, Butler]</p>
<p id="m1">1 true true false</p>
<p id="ag1">29.75 9.9166666667</p>
<p id="ag2">962 320.6666666667</p>
Source: Using Thymeleaf — Arrays, Using Thymeleaf — Lists, Using Thymeleaf — Sets, Using Thymeleaf — Maps, Using Thymeleaf — Aggregates
#lists.sort returns a sorted copy and leaves the model list untouched, and #sets.toSet of the author list removes the
duplicate "Asimov". #aggregates.avg on BigDecimal values returns a BigDecimal printed here with ten fraction digits, so round it with #numbers.formatDecimal before showing it. The isEmpty helpers of lists, sets, maps and arrays returned true for a null argument in the scratch run; #lists.size(null) throws (see the pitfalls).
Unique identifiers: #ids
Inside an iteration the same id would be repeated for every row, which makes HTML invalid and breaks <label for>. #ids
solves that by keeping a counter per name:
-
${#ids.seq('copy')}returns the name followed by the next counter value (copy1,copy2, …), forth:id. -
${#ids.next('copy')}returns the identifier the nextseqcall will produce, for a<label>written before its input. -
${#ids.prev('copy')}returns the identifier the lastseqcall produced, for a<label>written after its input.
<ul id="ids1">
<li th:each="a : ${authors}">
<input type="checkbox" th:id="${#ids.seq('author')}">
<label th:for="${#ids.prev('author')}" th:text="${a}">x</label>
</li>
</ul>
<div id="ids2"><label th:for="${#ids.next('copy')}">Copy</label><input th:id="${#ids.seq('copy')}"></div>
<li>
<input type="checkbox" id="author1">
<label for="author1">Le Guin</label>
</li>
<li>
<input type="checkbox" id="author2">
<label for="author2">Asimov</label>
</li>
<div id="ids2"><label for="copy1">Copy</label><input id="copy1"></div>
Source: Using Thymeleaf — IDs
The Thymeleaf + Spring tutorial uses the same two methods in forms, where th:field generates the ids of repeated checkboxes and radio buttons and the label is paired with #ids.prev or #ids.next; the Forms & Data Binding page shows that pairing.
Additions from the Spring integration: #fields, #mvc, #themes
When Thymeleaf is used through thymeleaf-spring6 (Spring MVC and WebFlux) the SpringStandardDialect replaces OGNL with Spring EL
and registers three more expression objects. They are not available in a standalone engine.
| Object | What it does |
|---|---|
|
Form-binding errors. |
|
|
|
|
#fields and #mvc were run in the scratch application. The form is bound to a Book whose title is @NotBlank, and it is
posted with an empty title:
<form th:object="${book}" th:action="@{/util/fields}" method="post">
<input type="text" th:field="*{title}">
<p id="f1" th:text="${#fields.hasErrors('title')}">x</p>
<p id="f2" th:text="${#fields.hasAnyErrors()}">x</p>
<ul id="f3"><li th:each="err : ${#fields.errors('title')}" th:text="${err}">x</li></ul>
<ul id="f4"><li th:each="e : ${#fields.detailedErrors()}" th:text="${e.fieldName} + ': ' + ${e.message}">x</li></ul>
</form>
<p id="mvc1" th:text="${#mvc.url('UC#demo').build()}">x</p>
<p id="mvc2" th:text="${#mvc.url('UC#showBook').arg(0, 42L).build()}">x</p>
<p id="f1">true</p>
<p id="f2">true</p>
<ul id="f3"><li>must not be blank</li></ul>
<ul id="f4"><li>title: must not be blank</li></ul>
<p id="mvc1">/util</p>
<p id="mvc2">/util/books/42</p>
Source: Thymeleaf + Spring — Validation and Error Messages, Thymeleaf + Spring — Building URIs to controllers, Thymeleaf + Spring — The SpringStandard Dialect
UC#demo is the name Spring MVC derives for a mapped method: the capital letters of the controller class name (UtilityController
becomes UC), a # and the method name. The .arg(0, 42L) argument must be of the method’s exact parameter type, so the long
parameter needs a long literal; a plain 42 is an Integer and fails with a conversion error. The Validation & Error Messages page covers #fields in depth, and the Link URLs page covers #mvc.url.
On #themes, the object is still registered by thymeleaf-spring6 3.1.5, but Spring Framework 7.0.9 no longer ships theme
support (spring-context 7.0.9 has no org.springframework.ui.context package), so there is no theme source for it to read on the
Boot 4 stack. The tutorials still show it; do not plan new work around it, and use message bundles and CSS for look-and-feel
switching.
Additions from Spring Security: #authentication and #authorization
With thymeleaf-extras-springsecurity6 on the classpath (Spring Boot registers its dialect automatically when Spring Security is
present) two more expression objects exist, plus the sec:* attributes:
-
#authenticationis the Spring SecurityAuthenticationof the current request, so${#authentication.name}is the user name and${#authentication.authorities}the granted authorities. -
#authorizationoffersexpression('...')/expr('...')for a Spring Security web expression andurl('...')/url('METHOD', '...')for "may the user access this URL".
<p id="sec1" th:text="${#authentication.name} + ' ' + ${#authorization.expression('hasRole(''USER'')')} + ' ' + ${#authorization.expression('hasRole(''ADMIN'')')}">x</p>
<p id="sec2" th:text="${#authorization.url('GET', '/util')}">x</p>
<p id="sec1">user true false</p>
<p id="sec2">true</p>
Hiding a button is presentation; the server must still enforce the rule on the endpoint. The Spring Security Integration page covers the sec:authorize attributes, CSRF and the rest of the dialect.
Where utility objects run, and restricted mode
Utility objects run during the processing phase, when the engine evaluates the attribute that holds the expression. They need no
preprocessing, and a template that uses them stays a natural template: the static prototype keeps the placeholder text
(<p th:text="${#temporals.format(published)}">May 1, 1974</p>) and the server replaces it with the computed value.
Most utility objects are also allowed in restricted expression mode, the stricter evaluation used for th:utext, preprocessing
__...__, th:on* handlers, fragment names and URL bases (Appendix D of the tutorial lists the contexts). In the scratch run,
#strings.toUpperCase(name) and #uris.escapePath(name) evaluate normally inside th:utext, but #execInfo is rejected
there, as shown above. The rule of thumb is that restricted mode forbids objects that expose the engine’s internals, request
parameters, static access, object instantiation and Spring bean references, and leaves pure value helpers alone. Restricted mode is
defence in depth and does not replace validating data; the Template Security & XSS page treats it fully.
Version notes and where the books are outdated
-
#temporalsin core. Since Thymeleaf 3.1#temporalsis part of core. Material for 3.0 (older articles and the books) asks forthymeleaf-extras-java8time; that module is frozen at 3.0.4 and is not needed with Thymeleaf 3.1 or Spring Boot 4. -
Removed request-related basic objects. Thymeleaf 3.1 removed the servlet-bound
#request,#response,#sessionand#servletContextexpression objects, and the${execInfo}variable form. The utility objects of this page are unaffected;#execInfowith the#is the current spelling. See the What’s Changed & Migration page. -
Package and module names. In the Spring integration the classes behind
#fields,#mvcand#themeslive inorg.thymeleaf.spring6.expression, inthymeleaf-spring6; there is nothymeleaf-spring7artifact, and Spring Boot 4.1.1 usesthymeleaf-spring6. -
Books. The Modern frontends with htmx chapter on out-of-band swaps formats dates in a weekly view with
#temporals.format. The call is the same today, and on Thymeleaf 3.1 it needs no extra dependency; pass#locale(or a pattern with a locale) when weekday and month names must follow the reader, as shown above.
Pitfalls
| Mistake | What happens | Fix |
|---|---|---|
|
|
Use |
|
The mirror error: no |
Use |
|
|
Guard with |
Formatters on |
Render an empty string, no error |
Intended; add |
|
No error: a lenient calendar rolls the month over into January 2027 |
Validate inputs; months are 1 to 12 |
|
|
Use |
|
Double-escaped text such as |
Drop the helper; |
|
A generic currency sign instead of |
Provide a region ( |
Hand-built URLs with |
Easy to miss an escaper for the right URI part |
Use |
|
|
Compute it in the controller, or move it to |
|
The expression object does not exist |
They come from |
|
No theme source is available |
Drop theme usage |