I want to create a recursive template with Thymeleaf 3.1.2. (The full code is below).
I have a recursive Node class:
public static class Node {
public String description2;
public List<Node> children2;
public List<Node> getChildren() {
return children2;
}
public String getDescription() {
return description2;
}
}
The recursive structure is very simple and has no children:
Node root = new Node();
root.description2 = "The root";
root.children2 = List.of();
The template:
<div th:insert="~{this :: fragmentForNode(${root})}"></div>
<div th:fragment="fragmentForNode(node)">
<span th:text="${node.description}"></span>
<ul th:if="${!node.children.isEmpty()}">
<li th:each="child : ${node.children}">
<div th:insert="~{this :: fragmentForNode(${child})}"></div>
</li>
</ul>
</div>
When I run this test, I see that getDescription is called, then getChildren, then isEmpty.
I then get the following exception which I can’t explain because there is an if condition that should stop the recusion.
org.thymeleaf.exceptions.TemplateProcessingException: Exception evaluating OGNL expression: "node.description" (template: " <!DOCTYPE html>
<html xmlns:th="http://www.thymeleaf.org">
<head>
<title>Help!</title>
</head>
<body>
<div th:insert="~{this :: fragmentForNode(${root})}"></div>
<div th:fragment="fragmentForNode(node)">
<span th:text="${node.description}"></span>
<ul th:if="${!node.children.isEmpty()}">
<li th:each="child : ${node.children}">
<div th:insert="~{this :: fragmentForNode(${child})}"></div>
</li>
</ul>
</div>
</body>
</html>
" - line 10, col 11)
at
...
Caused by: ognl.OgnlException: source is null for getProperty(null, "description")
After digging a bit more by creating a more complex hierarchy, I got the template working by patching with a check ${node} != null. It is like the fragment definition is also a call to the fragment.
<div th:if="${node} != null" th:fragment="fragmentForNode(node)">
<span th:text="${node.description}"></span>
<ul th:if="${!node.children.isEmpty()}">
<li th:each="child : ${node.children}">
<div th:replace="~{this :: fragmentForNode(${child})}">
</div>
</li>
</ul>
</div>
Here is the full code:
import java.util.List;
import org.thymeleaf.ITemplateEngine;
import org.thymeleaf.TemplateEngine;
import org.thymeleaf.context.Context;
import org.thymeleaf.templateresolver.StringTemplateResolver;
public final class RecurseTest {
private static final String HTML = """
<!DOCTYPE html>
<html xmlns:th="http://www.thymeleaf.org">
<head>
<title>Help!</title>
</head>
<body>
<div th:insert="~{this :: fragmentForNode(${root})}"></div>
<div th:fragment="fragmentForNode(node)">
<span th:text="${node.description}"></span>
<ul th:if="${!node.children.isEmpty()}">
<li th:each="child : ${node.children}">
<div th:insert="~{this :: fragmentForNode(${child})}"></div>
</li>
</ul>
</div>
</body>
</html>
""";
private static ITemplateEngine createTemplateEngine() {
StringTemplateResolver templateResolver = new StringTemplateResolver();
TemplateEngine templateEngine = new TemplateEngine();
templateEngine.setTemplateResolver(templateResolver);
return templateEngine;
}
public static class Node {
public String description;
public List<Node> children;
public List<Node> getChildren() {
System.err.println(description + ".getChildren");
return children;
}
public String getDescription() {
System.err.println(description + ".getDescription");
return description;
}
}
private static Node createHierarchy() {
Node root = new Node();
root.description = "The root";
root.children = List.of();
return root;
}
public static void main(String[] args) {
Context context = new Context();
context.setVariable("root", createHierarchy());
String html = createTemplateEngine().process(HTML, context);
System.err.print(html);
}
}
The issue you’re encountering in your Thymeleaf template stems from a mismatch between the property names you’re using in the template and the actual field names in your Node
class. Specifically, in your Thymeleaf template, you are referencing ${node.description}
and ${node.children}
, but in your code, the Node
class has the fields description2
and children2
.
To fix the issue, you need to either:
- Change the field names in your
Node
class todescription
andchildren
, or - Update the Thymeleaf template to reference
description2
andchildren2
to match your class fields.
I’ll walk you through both approaches:
1. Fixing the Node
Class Fields
Update your Node
class to have field names that match what you’re using in your template:
public static class Node {
public String description; // renamed to 'description'
public List<Node> children; // renamed to 'children'
public List<Node> getChildren() {
return children;
}
public String getDescription() {
return description;
}
}
By doing this, your Thymeleaf template will correctly map to the description
and children
properties in your Node
class.
2. Fixing the Thymeleaf Template
If you prefer not to change the class structure, you can update the Thymeleaf template to use the correct field names (description2
and children2
):
<div th:insert="~{this :: fragmentForNode(${root})}"></div>
<div th:fragment="fragmentForNode(node)">
<span th:text="${node.description2}"></span> <!-- Changed to description2 -->
<ul th:if="${!node.children2.isEmpty()}"> <!-- Changed to children2 -->
<li th:each="child : ${node.children2}"> <!-- Changed to children2 -->
<div th:insert="~{this :: fragmentForNode(${child})}"></div>
</li>
</ul>
</div>
Why is This Happening?
The error source is null for getProperty(null, "description")
occurs because Thymeleaf is trying to access the description
property, but it can’t find it in the Node
object, which results in null
. Since Thymeleaf couldn’t find the property, it throws this exception. This happens because the names don’t match.
By either renaming your class fields to description
and children
or updating the Thymeleaf template to use description2
and children2
, the error will be resolved.
Once you’ve made these changes, your code should work as expected without throwing any exceptions.
Lea is a new contributor to this site. Take care in asking for clarification, commenting, and answering.
Check out our Code of Conduct.
1