Standard Expression Syntax

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-07, and every example was compiled and run on that stack in a scratch Bookshelf project (MockMvc for the Spring examples, a plain TemplateEngine for the standalone ones). The Standard Expression language is shared by all the ways of using the engine, so the page applies to Thymeleaf standalone, with Spring MVC and with Spring WebFlux; the differences are the expression engine (OGNL standalone, Spring EL in Spring) and the web objects, and the text says where each one applies. The page is the overview of the language: the five expression types, how the variable and selection expressions navigate objects, what is available without being added to the model, and where the language stops being permissive. Literals and operators, preprocessing, link URLs, fragments and the utility objects have pages of their own in this section and are only previewed here.

The five expression types

Almost every value of a th:* attribute is a Standard Expression. Thymeleaf’s own "5 minutes" article defines five kinds, and the tutorial’s quick summary adds the literals and operators that can be combined with them. Each kind has its own delimiter and its own resolver, which is the important thing to remember: the delimiter tells the engine where the value will come from.

Table 1. The five simple expressions and the component that resolves each one
Type Syntax Resolved by What it produces

Variable

${…}

OGNL (standalone) or Spring EL (Spring), run against the context variables

Any object: a property, a map entry, a method result, a utility object

Selection

*{…}

The same engine, run against the object chosen with th:object

The same kind of value, relative to the selection

Message

#{…}

A message resolver (Spring’s MessageSource in Spring applications) and the context locale

A localized text, optionally with parameters

Link

@{…}

The configured link builder (StandardLinkBuilder by default) plus the response’s URL encoding

A URL with context path, path variables, encoded parameters and session information

Fragment

~{…}

The template resolvers and the fragment selector

A piece of markup that can be inserted, replaced or passed around like any other value

The figure puts the types side by side with their resolvers. Everything on the left is text inside an attribute value; everything on the right runs on the server, while the template is being processed.

flowchart LR subgraph attr["Attribute values in the template"] v["Variable ${...}"] s["Selection *{...}"] m["Message #{...}"] l["Link @{...}"] f["Fragment ~{...}"] end subgraph srv["Resolved on the server while the template is processed"] ctx[("Context variables
model attributes, request attributes,
param, session, application, #objects")] sel["Selected object
set by th:object"] msg["Message resolver
MessageSource + locale"] lnk["Link builder
context path, rewriting, encoding"] tpl["Template resolvers
+ fragment selector"] end v -->|"OGNL or Spring EL"| ctx s -->|"OGNL or Spring EL"| sel sel -.->|"no selection: falls back to"| ctx m --> msg l --> lnk f --> tpl ctx --> out["Text written into the HTML"] sel --> out msg --> out lnk --> out tpl --> out

The simple expressions nest and combine with the literals and operators. The tutorial’s own example mixes a conditional, a method call, a property and a literal in one value, and the rest of the language (concatenation, arithmetic, comparisons, the Elvis operator, the no-operation token, |…| substitution, double-brace conversion and __…__ preprocessing) is covered by the Literals, Operators & Preprocessing page of this section. The next example shows all five types in one small piece of the Bookshelf.

A template that uses the five expression types
<article th:object="${volume}">
  <h2 th:text="*{title}">Prototype title</h2>
  <p th:text="#{expr.volume.written(*{writer.name})}">Written by Someone</p>
  <a href="detail.html" th:href="@{/volumes/{id}(id=${volume.id},lang='en')}">Details</a>
  <div th:replace="~{expr/fragments :: badge(*{title})}">Badge</div>
</article>

The controller only has to put a volume in the model; the messages come from messages.properties (expr.volume.written=Written by {0}, and a Spanish messages_es.properties with Escrito por {0}) and the fragment is a one-line th:fragment="badge(label)". Rendered for a Spanish request, the page is:

Rendered output (Spanish locale)
<article>
  <h2>Dune</h2>
  <p>Escrito por Frank Herbert</p>
  <a href="/volumes/7?lang=en">Details</a>
  <span class="badge">Dune</span>
</article>

Four things happened that are worth reading twice. The th:object attribute did not appear in the output, because attributes of the th prefix are consumed by the engine. The selection *{title} found title on the volume, and the message parameter *{writer.name} used the same selection. The link expression added the path variable 7 and a query parameter, and, in an application deployed under a context path, would have prefixed it. The fragment expression replaced the whole div by the span of the fragment.

Variable expressions: OGNL and Spring EL

A variable expression ${…} is evaluated against the context variables, the map that the engine receives with the template. In a Spring MVC controller that map is the Model: Spring calls its entries model attributes, Thymeleaf calls them context variables, and they are the same thing. The tutorial’s equivalent in plain Java is ctx.getVariable("today"), and ${volume.writer.name} is a getWriter().getName() chain on the object found under the name volume.

The language behind the delimiters depends on the dialect:

  • The Standard dialect (a plain TemplateEngine, the standalone case) evaluates ${…} and *{…} as OGNL.

  • The SpringStandard dialect, which SpringTemplateEngine and therefore Spring Boot use, evaluates them as Spring EL (SpEL), with the syntax identical to OGNL for the common cases. Spring’s own tutorial lists this, with the ${@myBean.doSomething()} bean syntax, as the first features SpringStandard adds. The Spring EL compiler can be switched on with the spring.thymeleaf.enable-spring-el-compiler property, which defaults to false (see the Spring Boot Auto-configuration & Properties page).

Navigating objects

Both languages navigate a property path with dots (a getter call), with brackets and a quoted name, and with brackets and an unquoted index for lists and arrays. Maps accept both the dot and the bracket form, and methods can be called with or without arguments. SpEL adds a few operators that OGNL does not have, the most useful being the safe-navigation operator ?. and the bean reference @name.

Navigating a model with Spring EL
<ul>
  <li id="property" th:text="${volume.writer.name}">Someone</li>
  <li id="bracket" th:text="${volume['writer']['name']}">Someone</li>
  <li id="map" th:text="${volume.stock.main}">0</li>
  <li id="mapKey" th:text="${volume.stock['annex']}">0</li>
  <li id="list" th:text="${volume.tags[0]}">tag</li>
  <li id="array" th:text="${shelves[1]}">shelf</li>
  <li id="call" th:text="${volume.writer.signed('-')}">x</li>
  <li id="noargs" th:text="${volume.writer.initials()}">x</li>
  <li id="safe" th:text="${orphan.writer?.name}">no writer</li>
  <li id="safeDefault" th:text="${orphan.writer?.name} ?: 'unknown'">no writer</li>
  <li id="bean" th:text="${@catalog.featuredTitle()}">title</li>
  <li id="selection" th:text="${volumes.?[writer.born lt 1950].size()}">0</li>
  <li id="projection" th:text="${#strings.listJoin(volumes.![title], ', ')}">titles</li>
  <li id="static" th:text="${T(java.lang.Math).max(2, 5)}">0</li>
</ul>

With a volume whose writer is Frank Herbert (born 1920) and an orphan volume that has no writer, the page renders the following values. The catalog bean is an ordinary @Component("catalog") with a featuredTitle() method.

Table 2. Results of the Spring EL expressions
Item Rendered value

property, bracket

Frank Herbert

map, mapKey

4 and 0 (the main and annex stock)

list, array

sci-fi, poetry

call, noargs

Frank Herbert-1920 and F

safe

Empty text: the ?. operator turned a missing writer into null, and th:text renders null as nothing

safeDefault

unknown

bean

Dune

selection

1 (the collection selection .?[…] kept the volumes whose writer was born before 1950)

projection

Dune (the projection .![…] collected the titles)

static

5 (T(...) reaches a static method; allowed here for java.lang.Math)

Dereferencing a null in the middle of a path is an error in both languages unless the safe-navigation operator is used. Spring EL raises EL1007E: Property or field 'name' cannot be found on null, and the SpEL ?. form avoids it. A missing orphan.writer is a normal situation for an optional relation, so the pattern worth remembering is ${orphan.writer?.name} ?: 'unknown', safe navigation followed by the Elvis operator (described on the Literals, Operators & Preprocessing page).

The same navigation in OGNL

In a standalone engine the expressions are OGNL. The next test builds a TemplateEngine with a StringTemplateResolver, fills a Context and processes small templates, so the behaviour of each expression can be seen directly.

Evaluating expressions with the standalone (OGNL) engine
StringTemplateResolver resolver = new StringTemplateResolver();
resolver.setTemplateMode(TemplateMode.HTML);
TemplateEngine engine = new TemplateEngine();
engine.setTemplateResolver(resolver);

Context ctx = new Context(Locale.forLanguageTag("es"));
ctx.setVariable("volume", volume);
ctx.setVariable("orphan", orphanVolume);
ctx.setVariable("volumes", List.of(volume));

String html = engine.process("<p th:text=\"${volume.writer.name}\">x</p>", ctx);

Running a list of expressions through that engine gave these results, which differ from Spring EL in the places that matter:

Table 3. Results with OGNL on Thymeleaf 3.1.5
Expression Result

${volume.writer.name}, ${volume['writer']['name']}, ${volume.stock.main}, ${volume.tags[0]}

Work as in Spring EL

${volume.writer.signed('-')}, ${volume.writer.initials()}

Method calls work as in Spring EL

${orphan.writer.name}

OgnlException: source is null for getProperty(null, "name")

${orphan.writer?.name}

Template parsing error: OGNL has no ?. operator

${@catalog.featuredTitle()}

Template parsing error: there are no bean references outside Spring

${volumes.{? #this.writer.born lt 1950}.size()}

1 (OGNL’s own selection syntax)

${volumes.{title}}

[Dune] (OGNL’s own projection syntax)

${@java.lang.Math@max(2, 5)}

5 (OGNL’s syntax for a static method)

OGNL is documented in its own language guide; in a Spring application the guide is not needed, because the Spring EL reference applies. Choose one language per project and keep to the common subset (dotted paths, brackets, method calls) when templates must stay portable between standalone and Spring rendering.

Spring beans from a template

${@catalog.featuredTitle()} calls a method on the bean named catalog of the application context. The "how to access data" article shows the same pattern with a urlService bean. It is convenient for small look-ups (the current application URL, a feature flag), but it puts a service call in the view: a template that calls beans is harder to test and can run queries while rendering. Prefer to put the data into the model in the controller, and use @bean for stateless helpers. The restricted contexts below forbid bean access.

Expression basic objects and web namespaces

Besides the context variables, a few objects are always available to variable expressions, referenced with a leading #. Appendix A of the tutorial calls them the base objects:

  • #ctx is the context object itself: an implementation of IContext, or of IWebContext in a web environment. It is the recommended entry point.

  • #vars and #root are synonyms of the same object in the tutorial, which recommends #ctx.

  • #locale gives direct access to the java.util.Locale of the current request.

In a web environment there are also three namespaces. They are not context objects but maps added to the context as variables, so they are written without a #:

  • param holds the request parameters. ${param.foo} is a String[], so ${param.foo[0]} is normally the first value.

  • session holds the session attributes: ${session.foo}.

  • application holds the servlet context attributes: ${application.foo}.

Request attributes need no namespace: all of them are added to the context root, so a model attribute named volume is ${volume}. Each namespace also supports size(), isEmpty() and containsKey('foo').

Basic objects and namespaces in a Spring MVC template
<ul>
  <li id="locale" th:text="${#locale.language}">en</li>
  <li id="ctxLocale" th:text="${#ctx.getLocale()}">en</li>
  <li id="varNames" th:text="${#ctx.containsVariable('volume')}">x</li>
  <li id="vars" th:text="${#vars.getVariable('volume').title}">x</li>
  <li id="root" th:text="${#root.volume.title}">x</li>
  <li id="reqAttr" th:text="${volume.title}">x</li>
  <li id="param" th:text="${param.q}">q</li>
  <li id="param0" th:text="${param.q[0]}">q</li>
  <li id="paramNone" th:text="${param.missing}">none</li>
  <li id="paramSize" th:text="${param.size()}">0</li>
  <li id="paramHas" th:text="${param.containsKey('q')}">x</li>
  <li id="session" th:text="${session.reader}">x</li>
  <li id="application" th:text="${application.siteName}">x</li>
  <li id="exReq" th:text="${#ctx.getExchange().getRequest().getMethod()}">x</li>
</ul>

The controller stores reader in the session and a siteName attribute in the servlet context at start-up. A request to /expr/objects?q=a&q=b renders:

Rendered output (request with q=a and q=b)
<ul>
  <li id="locale">en</li>
  <li id="ctxLocale">en</li>
  <li id="varNames">true</li>
  <li id="vars">Dune</li>
  <li id="root">Dune</li>
  <li id="reqAttr">Dune</li>
  <li id="param">[a, b]</li>
  <li id="param0">a</li>
  <li id="paramNone"></li>
  <li id="paramSize">1</li>
  <li id="paramHas">true</li>
  <li id="session">Ada</li>
  <li id="application">Bookshelf</li>
  <li id="exReq">GET</li>
</ul>

Three details of that output come from running it rather than from the tutorial:

  • ${param.q} is the array, printed as [a, b]. Use an index for one value, and remember ${param.missing} is simply empty.

  • In Spring EL, #vars is the context (so getVariable('volume') works) while #root is the map of variables (so #root.volume works and getVariable does not). In OGNL both forms work. This is one more reason to follow the tutorial and write #ctx.

  • In both languages ${#ctx.locale}, which Appendix A lists, rendered as an empty string on 3.1.5: a property read on the context object is looked up as a context variable called locale. ${#locale} or the method form ${#ctx.getLocale()} returns the locale. The same applies to #ctx.exchange: in Spring EL write #ctx.getExchange().

The figure lists what a ${…} expression can reach, from the most specific to the shared sources.

flowchart TB expr["Variable expression ${...}"] expr --> own["Context variables
${volume}, ${requestPath}
(model attributes = request attributes)"] expr --> ns["Web namespaces
${param.q}, ${session.reader}, ${application.siteName}"] expr --> base["Base objects
${#ctx}, ${#vars}, ${#root}, ${#locale}"] expr --> util["Utility objects
${#strings}, ${#dates}, ${#lists}, ..."] expr --> bean["Spring beans (SpEL only)
${@catalog.featuredTitle()}"] base --> web["#ctx.getExchange()
IWebExchange: request, session, application"]

What 3.1 removed: the web-API expression objects

Before Thymeleaf 3.1, templates could write ${#request.requestURI}, ${#session.id} or ${#servletContext.contextPath}. The 3.1 "what’s new" article (now only available as an archived copy, since the page has disappeared from thymeleaf.org) lists the removal of the web-API based expression utility objects: #request, #response, #session and #servletContext are no longer available to expressions in Thymeleaf 3.1. The reason is the new web abstraction of that release: the engine no longer talks to the Servlet API (or its javax.* and jakarta.* variants) directly, but through the IWebApplication, IWebExchange, IWebRequest and IWebSession interfaces, and a template should not depend on a particular web API. The release also tightened which classes expressions may use, so reaching for HttpServletRequest internals is no longer the supported route anyway.

A template that still uses the old objects fails when it is rendered:

A 3.0-era template that no longer works
<p th:text="${#request.requestURI}">/path</p>

On 3.1.5 the request ends with a TemplateProcessingException whose root cause is an IllegalArgumentException: The 'request', 'session', 'servletContext' and 'response' expression utility objects are no longer available by default for template expressions and their use is not recommended. In cases where they are really needed, they should be manually added as context variables.

The replacements, from most to least recommended, are the following. They were all run on the Spring MVC example, with a request to /expr/replacement carrying an Accept-Language: es header and a session attribute reader.

  1. Put the value in the model. If a page needs the request URI, the controller adds it, which also makes the controller easy to test.

  2. Use the namespaces param, session and application for parameters, session attributes and servlet context attributes, exactly as the "how to access data" article does.

  3. Use the exchange from #ctx when the template really needs request data: IWebExchange exposes the request (method, path, headers, parameters, cookies), the session and the application, with getAttributeValue(…) on each.

  4. Add the object to the model yourself, as the error message says, when a legacy template cannot be rewritten. The object is then a plain variable, and it is the application’s responsibility to restrict what it exposes.

Replacing the removed objects
@GetMapping("/expr/replacement")
public String replacement(Model model, HttpServletRequest request) {
    model.addAttribute("requestPath", request.getRequestURI());
    return "expr/replacement";
}
<ul>
  <li id="model" th:text="${requestPath}">/path</li>
  <li id="path" th:text="${#ctx.getExchange().getRequest().getPathWithinApplication()}">/path</li>
  <li id="method" th:text="${#ctx.getExchange().getRequest().getMethod()}">GET</li>
  <li id="header" th:text="${#ctx.getExchange().getRequest().getHeaderValue('Accept-Language')}">x</li>
  <li id="exSession" th:text="${#ctx.getExchange().getSession().getAttributeValue('reader')}">x</li>
  <li id="exApp" th:text="${#ctx.getExchange().getApplication().getAttributeValue('siteName')}">x</li>
  <li id="sessionMap" th:text="${session.reader}">x</li>
</ul>
Rendered output
<ul>
  <li id="model">/expr/replacement</li>
  <li id="path">/expr/replacement</li>
  <li id="method">GET</li>
  <li id="header">es</li>
  <li id="exSession">Ada</li>
  <li id="exApp">Bookshelf</li>
  <li id="sessionMap">Ada</li>
</ul>

The IWebRequest methods used above (getMethod, getPathWithinApplication, getHeaderValue) and the getAttributeValue of IWebSession and IWebApplication are the public interface of the 3.1 abstraction. The Spring forms are written as method calls because, as seen above, a property read on the context object is looked up as a variable.

Selection expressions and th:object

A selection expression *{…} is a variable expression that is evaluated on a selected object instead of the whole context. The object is chosen by the result of a th:object attribute, and it is selected for the element that carries the attribute and everything inside it. As long as nothing is selected, *{…} and ${…} do the same thing, because the selection defaults to the whole context. Inside a selection the chosen object is also available as ${#object}, and ${…} and *{…} can be mixed freely.

Selection with th:object
<div th:object="${volume}">
  <p id="a" th:text="*{title}">t</p>
  <p id="b" th:text="${volume.writer.name}">w</p>
  <p id="c" th:text="${#object.title}">t</p>
  <div th:object="${#object.writer}">
    <p id="d" th:text="*{name}">n</p>
    <p id="e" th:text="${#object.born}">n</p>
  </div>
  <p id="f" th:text="*{title}">t</p>
</div>
<p id="g" th:text="*{volume.title}">t</p>

The nested th:object replaces the selection only for its own subtree, so after the inner div the paragraph f is again about the volume, and the paragraph g, outside any selection, resolves *{volume.title} against the whole context.

Rendered output
<div>
  <p id="a">Dune</p>
  <p id="b">Frank Herbert</p>
  <p id="c">Dune</p>
  <div>
    <p id="d">Frank Herbert</p>
    <p id="e">1920</p>
  </div>
  <p id="f">Dune</p>
</div>
<p id="g">Dune</p>

In Spring applications th:object has a second job: SpringStandard replaces it with an implementation that selects the form-backing (command) object, and it is then paired with th:field="*{title}", th:errors and the other form attributes. That implementation only accepts a variable expression. Writing th:object="*{writer}" in Spring fails while the template is parsed, with the message that only variable expressions (${…}) are allowed in th:object attributes in Spring-enabled environments, which is why the nested example above uses ${#object.writer}. The form attributes are the subject of the Forms & Data Binding page; its Bookshelf form is simply:

<form th:action="@{/books}" th:object="${book}" method="post">
  <input type="text" th:field="*{title}">
  <span th:if="${#fields.hasErrors('title')}" th:errors="*{title}">err</span>
  <input type="text" th:field="*{author}">
  <button type="submit">Save</button>
</form>

How and when expressions are evaluated

Expressions are not evaluated by the browser, and they are not evaluated all at once. The engine reads the template, finds each attribute that a processor handles, and the processor evaluates its own value when its turn comes. A few consequences follow.

  • Per attribute. th:text="${a}" and th:class="${b}" on the same element are two separate evaluations, run in the order set by the attributes' precedence (described on the Local Variables, Attribute Precedence, Comments & Blocks page of this section). A local variable defined by an earlier attribute, such as th:each or th:with, is visible to the later ones on the same element.

  • Parsed once, evaluated each time. The engine parses the text of an expression into an expression tree and caches it (the engine’s expression cache), so the cost on a request is evaluation, not parsing. Each Context supplies the variables of one execution.

  • Server-side only. The th:* attributes are consumed and never reach the browser, and the values in the prototype stay in the file: the text Prototype title or /path above is what a designer sees when the file is opened directly, which is what makes templates natural.

  • Every attribute knows its own kind of value. th:text accepts any expression; th:href and th:src expect a link expression (a variable expression can be wrapped as the base of one, @{${url}(id=${o.id})}); th:insert and th:replace expect a fragment expression; th:object in Spring expects a variable expression; th:field expects a selection expression.

Restricted mode

Thymeleaf does not let an expression do everything. Appendix D of the tutorial describes two kinds of restrictions:

  • General restrictions, applied to every expression: static use (instantiation or static calls) of most classes from Thymeleaf’s, Spring’s and Java/Jakarta’s base infrastructure is forbidden, together with a list of well-known third-party packages. The 3.1 article names the core packages (java.*, javax.*, jakarta.*, jdk.* and similar) and the exceptions that stay allowed, such as the basic java.lang and java.math classes, the collection interfaces and java.util.Date, Calendar, Locale, Optional and Properties. This is why T(java.lang.Math).max(2, 5) worked above.

  • Restricted expression evaluation mode, stricter and applied only in specific contexts. The tutorial states that it is an additional defence in depth and not a substitute for validating and sanitizing user input.

Restricted mode applies when an expression is evaluated in one of these places:

  1. preprocessing expressions, __…__

  2. unescaped output: th:utext and unescaped inlined expressions

  3. th:on* JavaScript event handler attributes whose value is a Standard Expression

  4. th:attr when it creates attributes with arbitrary names, and the default attribute processor, which renders any attribute with an arbitrary name

  5. template names and fragment parameters in fragment expressions and in th:insert, th:replace, th:include and th:substituteby

  6. the base (non-parameter) part of link URLs: @{…}, th:href and th:src

  7. every output expression in TEXT template mode, escaped or not

In that mode the following is forbidden: reading request parameters with ${param.*}, instantiating objects, static classes and their members (T(...) or OGNL’s @identifier@), some utility objects such as #execInfo, in th:on* attributes a variable expression whose result is not a number or boolean, and, in Spring applications, bean access with @beanName. A violation raises a template processing exception at runtime. The tutorial adds that these checks are a security feature, cannot be switched off per expression or per template, and that an expression which worked before an upgrade and now fails was most likely using the engine in a risky way; the recommended fix is to compute the data in the application code.

The 3.1 archive lists th:substituteby as removed, although Appendix D still names it among the attributes; do not rely on that attribute.

Restricted mode in practice
<p id="ok"  th:text="${@catalog.featuredTitle()}">title</p>
<p id="bad" th:utext="${@catalog.featuredTitle()}">title</p>

The first paragraph renders Dune. The second one fails when the template is parsed with a TemplateProcessingException: Instantiation of new objects and access to static classes or parameters is forbidden in this context (the same message is used for the bean access, the param access and the static access). The same happens with th:utext="${param.q}" and with a request parameter passed to a fragment, while a request parameter inside the parameter part of a link, or in a plain th:text, is not restricted:

Table 4. Contexts that are and are not restricted
Template fragment Result

<p th:utext="${param.q}">x</p>

Restricted: fails

<p th:utext="${#execInfo.templateName}">x</p>

Restricted: fails with Access to variable '#execInfo' is forbidden in this context

<div th:insert="~{expr/fragments :: badge(${param.q})}">x</div>

Restricted: fails

<a th:href="@{/search(q=${param.q})}">x</a>

Works: the URL parameter is not restricted (/search?q=hi)

<p th:text="${param.q}">y</p>

Works: renders the value, HTML-escaped

<p th:text="${T(java.lang.Math).max(1, 2)}">x</p>

Works: renders 2, because Math is an allowed class and th:text is not restricted

The Template Security & XSS page returns to this topic with the escaping rules and the 3.1.x advisories; the practical rule for now is that a user-controlled value belongs in th:text, never in th:utext, a preprocessing expression or an event handler.

Pitfalls

Table 5. Common mistakes with Standard Expressions
Mistake What happens Fix

${#request.requestURI} copied from a Thymeleaf 3.0 or earlier example

IllegalArgumentException in 3.1: the web-API objects are no longer available

Put the value in the model, or use param, session, application or #ctx.getExchange()

th:object="*{writer}" in a Spring application

Parse error: only variable expressions are allowed in th:object

Write th:object="${volume.writer}" or ${#object.writer}

${param.q} expecting a string

The value is a String[] and prints as [a, b]

Use ${param.q[0]}, or ${param.containsKey('q')} first

${#ctx.locale} or ${#ctx.exchange} in Spring EL

Empty result or EL1007E on null: the property is looked up as a context variable

Use ${#locale}, ${#ctx.getLocale()} and ${#ctx.getExchange()}

${orphan.writer.name} when the writer may be missing

EL1007E (Spring EL) or source is null (OGNL)

Use ${orphan.writer?.name} in Spring EL, or test with th:if; OGNL has no safe navigation

?. or @bean in a standalone engine

Template parsing error: both are Spring EL features

Add the value to the Context, or use OGNL’s own syntax

${@catalog.featuredTitle()} in th:utext

Fails: bean access is restricted

Put the value in the model and print it with th:text

${#vars} or ${#root} with Spring EL

The two are not the same object: #vars is the context, #root the variable map

Use #ctx, as the tutorial recommends

Calling services from the template

Queries run during rendering, and the template becomes hard to test

Load the data in the controller and expose it as model attributes

Where to go next

Two pages of this section build directly on this one and exist already: Introduction & Architecture explains the Standard and SpringStandard dialects and the processing pipeline in which these expressions are evaluated, and Spring Boot Auto-configuration & Properties documents the spring.thymeleaf.* properties, including the Spring EL compiler switch. The Literals, Operators & Preprocessing and Expression Utility Objects pages and the Link URLs and Fragments pages take each of the pieces previewed in the first table to full depth.