How to Trick Thymeleaf Into Thinking a String is a Number

03 Oct 2025

Table of Contents


To be fair, this is an OGNL issue, not a Thymeleaf issue.

If you are not familiar with OGNL, it’s the core expression language on which Thymeleaf is built.


The Trick

Here is a simple Thymeleaf template expression:

HTML
1
<div th:text="${letter == 'Z'}"></div>

We expect this to display <div>true</div> or <div>false</div>, depending on what we pass to the template:

Java
1
2
HashMap<String, Object> model = new HashMap<>();
model.put("letter", "A");

It actually throws an exception, with the following stack trace reason:

Caused by: java.lang.NumberFormatException: For input string: “A”

Wait, what? Why did that happen?

The Cause

OGNL attempts to compare "A" with 'Z'. In Java terms, this is a String compared to a char. OGNL categorizes char (and Character) as numeric types.

The OGNL code therefore attempts to cast the string "A" to a number, to facilitate the comparison with the char 'Z' - leading to the exception.

(If you’re really interested, you can see this happening in the OgnlOps.java source code.)

But why does OGNL consider char to be numeric? Because it is! char is one of Java’s integral types:

The values of the integral types are integers in the following ranges:

For byte, from -128 to 127, inclusive
For short, from -32768 to 32767, inclusive
For int, from -2147483648 to 2147483647, inclusive
For long, from -9223372036854775808 to 9223372036854775807, inclusive
For char, from ‘\u0000’ to ‘\uffff’ inclusive, that is, from 0 to 65535

Here, the word “integral” is used in the mathematical sense, meaning “of or denoted by an integer”.

Java’s char has something of a split personality. We generally understand what a “character” is, in its simplest meaning (it’s a letter of the alphabet!). But Java has a different understanding, given the reason char exists in Java:

The char data type (and therefore the value that a Character object encapsulates) are based on the original Unicode specification, which defined characters as fixed-width 16-bit entities.

OGNL Flexibility? OGNL Bug?

OGNL (and Thymeleaf) allows you to place a string inside single- or double-quotes. For Thymeleaf, that is very convenient, because you can use th:text="${word == 'foo'}". In that case, the literal 'foo' will be treated as a string by OGNL - and it will be compared to the string we provide in our model. No number format exception will be thrown.

But if the expression only contains a single character (th:text="${letter == 'Z'}"), then OGNL treats it as a char - and we get the exception.

Is this a bug? I don’t think so. I think it’s more a consequence of my code not anticipating the problematic comparison I am asking it to do.

Is it unfortunate? Absolutely, yes, because it’s an unpleasant surprise; not necessarily obvious what went wrong; and an easy mistake to make.

How to Avoid the Problem

OPTION 1: Try to remember to never use single-character literals in your templates - especially when performing comparisons using those literals. (Easier said than done.)

OPTION 2: If you need a string containing only a single character, you can use &quot; instead of a double quote ("):

HTML
1
<div th:text="${letter == &quot;Z&quot;}"></div>

Now, the Z is handled as a String by OGNL and Thymeleaf. But… yuck.

OPTION 3: Don’t use OGNL. For example, you can configure Thymeleaf to use SpEL (Spring’s expression language). SpEL does not have the same behavior. It will treat 'Z' as comparable with "A", without throwing an exception.

You don’t need to use the entire Spring framework to do this. See Spring 6 and Thymeleaf Without Spring.

More Troublesome Characters…

In modern Java, the char primitive is problematic for more fundamental reasons that the issue discussed here. There’s a great article about that by Cay Horstmann:

Stop Using char in Java. And Code Points.