Template Engines

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 Apache Groovy 6.0 (6.0.0, 2026-09-22) and every example was run with Groovy 6.0.0 on JDK 26 (checked on 2026-10-14). Groovy can build text with GStrings, printf and the markup builders, and for output that follows a fixed shape there is a dedicated template framework in the groovy-templates module (in the lib directory of the binary distribution, so no @Grab is needed). A template is text with placeholders and a little code; an engine compiles it once and renders it many times with different data. The module has five engines, all in the groovy.text package, and they differ in syntax, in streaming and in how much they know about markup. The page ends with the most capable one, the MarkupTemplateEngine. For text that is built once in code, plain GStrings (Strings & GStrings) and the XML builders (XML: Creating & Manipulating) are usually enough, and the builders in general are on Builders.

The template framework

The framework has two types. TemplateEngine is an abstract class with createTemplate methods that accept a String, a Reader, a File or a URL, and return a Template. Template is an interface with one method, make(Map binding), which returns a Writable. Calling toString() renders into a string and writeTo(Writer) streams the output, so a large report need not be built in memory. The rendering is lazy: a missing variable or an exception inside a scriptlet only surfaces when the Writable is written, not in make. A template is created once and made many times with different bindings.

Engine Syntax Use when

SimpleTemplateEngine

${expr}, $name, <% code %>, <%= expr %>

small text templates; the baseline

StreamingTemplateEngine

the same as Simple, plus out bound to the writer

templates whose static text exceeds 64k

GStringTemplateEngine

the same as Simple, compiled to writable closures

streaming output, templates kept in files

XmlTemplateEngine

XML with $name, ${expr} and gsp: tags

the template and the result are XML

MarkupTemplateEngine

the builder DSL (html { body { p('text') } })

HTML, XML and any markup; typed, fast, with layouts

SimpleTemplateEngine

SimpleTemplateEngine uses JSP-like syntax: <% ... %> runs Groovy code, <%= ... %> prints the value of an expression, and $name or ${expression} interpolates as in a GString. The binding’s keys become variables and print writes into the output. This template renders a small zone report from the greenhouse readings, then renders the same compiled template with another binding:

import groovy.text.SimpleTemplateEngine

def engine = new SimpleTemplateEngine()
def text = '''\
Zone: ${zone.toUpperCase()}
<% readings.each { r -> %>  ${r.sensor.padRight(8)} ${r.temperature}
<% } %>Average: <%= (readings*.temperature.sum() / readings.size()) %>
'''
def template = engine.createTemplate(text)
def out = template.make(zone: 'bench', readings: [
    [sensor: 'bench-1', temperature: 18.5],
    [sensor: 'bench-2', temperature: 20.0]])
print out
println '--'
// the same compiled template, another binding
print template.make(zone: 'bed', readings: [[sensor: 'bed-3', temperature: 17.25]])
Zone: BENCH
  bench-1  18.5
  bench-2  20.0
Average: 19.25
--
Zone: BED
  bed-3    17.25
Average: 17.25

A scriptlet can span lines and mix with static text, as the each loop above does: the closure opened in one <% %> is closed in a later one. Keep the logic small; the closing <% } %> placement controls the newlines in the result.

Three details cause most of the surprises:

import groovy.text.SimpleTemplateEngine

def engine = new SimpleTemplateEngine()
try {
    engine.createTemplate('Hello ${name}').make([:]).toString()
} catch (e) { println e.class.simpleName + ': ' + e.message.readLines()[0] }
// a default for a missing value: bind it, even as null
println engine.createTemplate('Hello ${name ?: "anonymous"}').make([name: null])
// the template string is parsed by Groovy first, so a backslash must be doubled
println engine.createTemplate('He said <% print "\\"hi\\"" %>').make()
// a literal dollar sign is escaped
println engine.createTemplate('Price: \\$5, ${5 * 2}').make()
MissingPropertyException: No such property: name for class: SimpleTemplateScript2
Hello anonymous
He said "hi"
Price: $5, 10

An unbound variable is an error, not an empty string, so give optional values a binding of null. The doubled backslash is needed only when the template is a Groovy string literal; a template read from a file is passed through once, as the GStringTemplateEngine example below shows. The Template object compiles the text into a script, so createTemplate is the expensive call and make is cheap; create the template once and cache it.

StreamingTemplateEngine

StreamingTemplateEngine accepts the same syntax and renders to the same result, but compiles the template into writable closures. The documented reason to choose it is size: SimpleTemplateEngine puts the static text in a script constant, and the class-file format limits a string constant to 65,535 characters. The binding’s out variable is the writer. This example runs both engines on a template with 70,000 characters of static text:

import groovy.text.*

def model = [zone: 'bench', level: 'high', readings: [[sensor: 'bench-1', temperature: 31.5], [sensor: 'bench-2', temperature: 33.0]]]

def streaming = new StreamingTemplateEngine().createTemplate('''\
Zone ${zone}: <% if (level == 'high') out.print 'open the vents' else out.print 'all clear' %>.
<% readings.each { r -> %>- ${r.sensor} at ${r.temperature}
<% } %>''')
print streaming.make(model)
println '--'

// A template body above 64k characters
def big = ('x' * 70000) + '${zone}\n'
println big.length()
println new StreamingTemplateEngine().createTemplate(big).make(model).toString().length()
try {
    println new SimpleTemplateEngine().createTemplate(big).make(model).toString().length()
} catch (Throwable e) { println e.class.simpleName + ": " + e.message.readLines()[0].take(90) }
Zone bench: open the vents.
- bench-1 at 31.5
- bench-2 at 33.0
--
70008
70006
GroovyRuntimeException: Failed to parse template script (your template may contain an error or be trying to use ex

The SimpleTemplateEngine failure is the compiler’s String too long. ... maximum of 65535 is allowed message, wrapped in a GroovyRuntimeException. Prefer StreamingTemplateEngine whenever the template text can grow, for example a template that is read from a file you do not control.

GStringTemplateEngine

GStringTemplateEngine also uses the Simple syntax, but stores the template as writable closures and expects output through out (use out << rather than print so it streams). It is the engine to use with template files. The template is a file, so the quote needs only one backslash:

Alert for "$zone" (${level.toUpperCase()})
<% readings.each { r -> out << "  ${r.sensor}: ${r.temperature}\n" } %>Action: <% out << (level == 'high' ? "\"open the vents\"" : 'none') %>
import groovy.text.GStringTemplateEngine

def model = [zone: 'bench', level: 'high', readings: [[sensor: 'bench-1', temperature: 31.5], [sensor: 'bench-2', temperature: 33.0]]]
def gt = new GStringTemplateEngine().createTemplate(new File('tpl/alert.template'))
print gt.make(model)
println '--'
// stream to a Writer without building a String
def w = new StringWriter()
gt.make(model).writeTo(w)
println w.toString().length()
Alert for "bench" (HIGH)
  bench-1: 31.5
  bench-2: 33.0
Action: "open the vents"
--
82

The Groovy documentation’s file example writes the quotes with two backslashes (\\"). With Groovy 6.0.0 that text did not compile (Unexpected character: '\\'), and the single-backslash form above is what worked, so test file templates rather than copying the escaping from inline strings.

XmlTemplateEngine

XmlTemplateEngine is for templates that are themselves XML, with $name and ${expression} in text and attributes, and two special tags in the gsp: namespace: gsp:scriptlet for code and gsp:expression for code whose value is printed. Comments and processing instructions are dropped, the gsp namespace declaration is removed, and the output is pretty printed with indentation.

import groovy.text.XmlTemplateEngine

def text = '''\
<report xmlns:gsp="http://groovy.apache.org/gsp" zone="$zone">
    <gsp:scriptlet>def hot = readings.findAll { it.temperature > 30 }</gsp:scriptlet>
    <gsp:expression>hot.size()</gsp:expression>
    <!-- dropped from output -->
    <note>$note</note>
</report>
'''
def model = [zone: 'bench', note: 'Vents "open" & <fans> on', readings: [[temperature: 31.5], [temperature: 18.0]]]
println new XmlTemplateEngine().createTemplate(text).make(model)
<report zone='bench'>
  1
  <note>
    Vents "open" & <fans> on
  </note>
</report>

Look closely at the note element: the values from the binding were inserted as they are, with no escaping, so the output is not well-formed XML. The documentation says special characters "will be escaped", but in this run that applied to literal characters of the template, not to interpolated values. Escape values yourself, with groovy.xml.XmlUtil.escapeXml(value) (which turns x < y & "z" into x < y & "z"), or use the MarkupTemplateEngine with autoEscape. The XML builders are on XML: Creating & Manipulating.

MarkupTemplateEngine

The MarkupTemplateEngine (package groovy.text.markup) works differently from the other four: templates are Groovy code in the builder style of Builders, and they are compiled to bytecode, so rendering is fast. It targets XML-like output (XML, XHTML, HTML5) but works for any text. Its main features are the builder syntax, compiled templates, optional type checking of the model, includes, internationalization, fragments and layouts.

The template format

A tag is a method call: arguments that are a map become attributes, a string argument becomes the text, and a closure becomes the nested content. Model variables are plain variables of the template.

import groovy.text.markup.*

def engine = new MarkupTemplateEngine()
def tpl = engine.createTemplate '''\
xmlDeclaration()
log(zone: zone) {
    readings.each { r ->
        reading(sensor: r.sensor, temperature: r.temperature)
    }
}
'''
def model = [zone: 'bench', readings: [[sensor: 'bench-1', temperature: 18.5], [sensor: 'bench-2', temperature: 20.0]]]
println tpl.make(model)

def html = engine.createTemplate '''\
yieldUnescaped '<!DOCTYPE html>'
html(lang: 'en') {
    head { title("Zone $zone") }
    body {
        h1(zone)
        table {
            readings.each { r ->
                tr { td(r.sensor); td(r.temperature) }
            }
        }
        comment 'generated'
        p {
            yield 'Raw: <b>'
            yieldUnescaped ' unescaped: <b>'
        }
    }
}
'''
println html.make(model)
<?xml version='1.0'?>
<log zone='bench'><reading sensor='bench-1' temperature='18.5'/><reading sensor='bench-2' temperature='20.0'/></log>
<!DOCTYPE html><html lang='en'><head><title>Zone bench</title></head><body><h1>bench</h1><table><tr><td>bench-1</td><td>18.5</td></tr><tr><td>bench-2</td><td>20.0</td></tr></table><!--generated--><p>Raw: &lt;b&gt; unescaped: <b></p></body></html>

Besides the tag methods, the template has support methods:

Method Effect

yield

renders text, escaping it

yieldUnescaped

renders text as it is

xmlDeclaration()

renders <?xml version='1.0'?>, with the encoding when declarationEncoding is configured

comment

renders its raw argument inside an XML comment

newLine()

renders a new line

pi

renders a processing instruction, for example pi("xml-stylesheet":[href:"mystyle.css", type:"text/css"])

tryEscape

returns the escaped string for a CharSequence, and any other object unchanged

Creating the engine and configuring it

The engine takes a TemplateConfiguration. createTemplate accepts a String, a Reader or a URL, and createTemplateByPath loads a template by name from the classpath, or from a directory with the constructor new MarkupTemplateEngine(classLoader, directory, config). Prefer the by-path form, which finds the includes and layouts too. The configuration is fixed once the engine exists; changing it afterwards is unsafe. The options and their defaults:

Option Default Effect

declarationEncoding

null

encoding written by xmlDeclaration()

expandEmptyElements

false

renders <p></p> instead of <p/>

useDoubleQuotes

false

double quotes around attribute values

newLineString

the line.separator property

the string newLine() renders

autoEscape

false

escape CharSequence values of the model

autoIndent

false

indent after new lines

autoIndentString

four spaces

the indent

autoNewLine

false

insert new lines where the template has braces

baseTemplateClass

groovy.text.markup.BaseTemplate

the superclass of compiled templates

locale

the default locale

the locale for template variants

cacheTemplates

true

reuse compiled templates (read back with engine.templateConfiguration.cacheTemplates)

The configuration object has a no-argument constructor with setters, so named arguments set options. With the formatting options the markup is pretty printed:

import groovy.text.markup.*

def cfg = new TemplateConfiguration(autoIndent: true, autoNewLine: true, useDoubleQuotes: true,
                                    expandEmptyElements: true, declarationEncoding: 'UTF-8')
def pretty = new MarkupTemplateEngine(cfg).createTemplate '''\
xmlDeclaration()
log {
    readings.each { r ->
        reading(sensor: r.sensor)
        empty()
    }
}
'''
println pretty.make(zone: 'bench', readings: [[sensor: 'bench-1'], [sensor: 'bench-2']])
<?xml version="1.0" encoding="UTF-8"?>
<log>
    <reading sensor="bench-1"></reading><empty></empty><reading sensor="bench-2"></reading><empty></empty>
</log>

autoNewLine follows the braces of the template source: a tag with a body in braces starts a new line, but tags written one after another inside the same body do not, as the reading and empty tags show. Call newLine() to force a break.

Includes, fragments and layouts

For the template files below, the engine reads a views directory:

  • layout-main.tpl is a page skeleton, with bodyContents() as the slot that pages fill in.

  • report.tpl is a page that calls it.

  • footer.txt is a text file with markup characters in it (Next check: <b>noon</b>).

// views/layout-main.tpl
html {
    head { title(title) }
    body {
        h1(title)
        bodyContents()
    }
}
// views/report.tpl
layout 'layout-main.tpl',
    title: "Zone $zone",
    bodyContents: contents {
        ul {
            readings.each { r ->
                fragment "li(text)", text: "${r.sensor}: ${r.temperature}"
            }
        }
        p { includeUnescaped 'footer.txt' }
        p { includeEscaped 'footer.txt' }
    }
import groovy.text.markup.*

def cfg = new TemplateConfiguration(autoNewLine: true, autoIndent: true)
def engine = new MarkupTemplateEngine(this.class.classLoader, new File('views'), cfg)
def model = [zone: 'bench', readings: [[sensor: 'bench-1', temperature: 18.5], [sensor: 'bench-2', temperature: 20.0]]]
println engine.createTemplateByPath('report.tpl').make(model)
<html>
    <head><title>Zone bench</title></head><body>
        <h1>Zone bench</h1><ul><li>bench-1: 18.5</li><li>bench-2: 20.0</li></ul><p>Next check: <b>noon</b></p><p>Next check: &lt;b&gt;noon&lt;/b&gt;</p>
    </body>
</html>

The three composition tools solve different problems:

  • Includes insert another file: include template: 'other.tpl' renders a template, include unescaped: 'raw.txt' inserts a file as it is, and include escaped: 'text.txt' inserts it escaped. includeGroovy(name), includeUnescaped(name) and includeEscaped(name) are the method forms, handy when the name is in a variable. Included files are found on the classpath (or in the configured directory).

  • Fragments are nested templates held in a string, rendered with their own model: fragment "li(text)", text: .... They avoid repeating markup, at the price of compiling one fragment per template, and they cannot be moved to a file.

  • Layouts refer to another template file. The layout receives only the values passed in the layout call, unless the call passes true as its second argument to inherit the model of the calling page. Wrap the content passed to the layout in contents { ... }, otherwise the block is rendered in place and shows a stray value:

// views/inherit.tpl
layout 'layout-main.tpl', true,
    bodyContents: contents { p('inherits the title') }
<html>
    <head><title>Inherited title</title></head><body>
        <h1>Inherited title</h1><p>inherits the title</p>
    </body>
</html>

That output came from engine.createTemplateByPath('inherit.tpl').make(title: 'Inherited title'). A key passed in the layout call overrides the inherited value of the same name.

Escaping

By default the markup engine renders model strings as they are. For text that comes from users that is a cross-site-scripting risk, so turn on autoEscape: every CharSequence from the model is escaped, and a template that needs the raw value says so with the unescaped prefix.

import groovy.text.markup.*

def src = 'div(note)'
def model = [note: 'Water <script>alert(1)</script> now']
println new MarkupTemplateEngine().createTemplate(src).make(model)
println new MarkupTemplateEngine(new TemplateConfiguration(autoEscape: true)).createTemplate(src).make(model)
println new MarkupTemplateEngine(new TemplateConfiguration(autoEscape: true)).createTemplate('div(unescaped.note)').make(model)
<div>Water <script>alert(1)</script> now</div>
<div>Water &lt;script&gt;alert(1)&lt;/script&gt; now</div>
<div>Water <script>alert(1)</script> now</div>

Markup inside a string

The engine streams: each tag writes to the output when its method is called. So a tag inside a GString is evaluated while the string is being built, before the string itself is written, which puts the markup in the wrong place. Use stringOf { ... }, the $tag shortcut, or separate yield calls:

import groovy.text.markup.*

def e = new MarkupTemplateEngine()
println e.createTemplate('p("See ${a(href: "#top", "top")} now")').make([:])
println e.createTemplate('p("See ${stringOf { a(href: "#top", "top") }} now")').make([:])
println e.createTemplate('p("See ${$a(href: "#top", "top")} now")').make([:])
println e.createTemplate('p { yield "See "; a(href: "#top", "top"); yield " now" }').make([:])
<a href='#top'>top</a><p>See  now</p>
<p>See <a href='#top'>top</a> now</p>
<p>See <a href='#top'>top</a> now</p>
<p>See <a href='#top'>top</a> now</p>

The first line is the misordered output. stringOf and $tag render to a separate string writer, which is slower than the direct streaming of the yield form.

Internationalization

A template can have one file per locale, named name_language_COUNTRY.tpl. When a template is rendered or included without a locale in its name, the engine looks for the variant of the configured locale and falls back to the unnamed file. A name with a locale in it asks for that exact variant. The directory has hello.tpl (p("Default text")), hello_fr_FR.tpl (the French text) and greet.tpl (include template: "hello.tpl"):

import groovy.text.markup.*

def cfg = new TemplateConfiguration()
def engine = new MarkupTemplateEngine(this.class.classLoader, new File('views'), cfg)
println engine.createTemplateByPath('hello.tpl').make([:])
println engine.createTemplateByPath('greet.tpl').make([:])
println engine.createTemplateByPath('hello_fr_FR.tpl').make([:])
cfg.locale = Locale.FRANCE
def fr = new MarkupTemplateEngine(this.class.classLoader, new File('views'), cfg)
println fr.createTemplateByPath('greet.tpl').make([:])
<p>Default text</p>
<p>Default text</p>
<p>Texte en français</p>
<p>Texte en français</p>

Translate one template at a time: the files without a locale remain the default for every variant that does not exist yet.

Custom template classes

Compiled templates extend BaseTemplate. Set another baseTemplateClass to add helper methods that every template can call.

import groovy.text.markup.*

abstract class GreenhouseTemplate extends BaseTemplate {
    GreenhouseTemplate(MarkupTemplateEngine engine, Map model, Map<String, String> modelTypes, TemplateConfiguration cfg) {
        super(engine, model, modelTypes, cfg)
    }
    String celsius(def value) { "${value} °C" }
}

def cfg = new TemplateConfiguration(baseTemplateClass: GreenhouseTemplate)
def engine = new MarkupTemplateEngine(this.class.classLoader, cfg)
println engine.createTemplate('p(celsius(temperature))').make(temperature: 18.5)
<p>18.5 °C</p>

Type-checked templates

Templates are statically compiled, but without model types a misspelled property is found only when that template is rendered. Give the engine the model types and the same mistake becomes a compile-time error. Either pass a map of types to createTypeCheckedModelTemplate (the types are strings), or declare them at the top of the template in a modelTypes block. The type-checked path also lets the compiler generate faster code. The class used by the model must be visible to the engine’s class loader, so the example passes the script’s loader.

import groovy.text.markup.*

class Plant { String name; String zone }

def engine = new MarkupTemplateEngine(this.class.classLoader, new TemplateConfiguration())
def plants = [new Plant(name: 'tomato', zone: 'bench-1')]

def ok = engine.createTemplate 'ul { plants.each { p -> li("${p.name} in ${p.zone}") } }'
println ok.make(plants: plants)

// untyped: the mistake only shows up on rendering
def bad = 'ul { plants.each { p -> li(p.nme) } }'
try { println engine.createTemplate(bad).make(plants: plants) } catch (e) { println 'render: ' + e.class.simpleName + ': ' + e.message.readLines()[0] }

// typed: fails at compile time
try {
    engine.createTypeCheckedModelTemplate(new StringReader(bad), [plants: 'List<Plant>'])
} catch (e) { println 'compile: ' + e.class.simpleName + ': ' + e.message.readLines().find { it.contains('nme') && it.contains('No such') } }

// types declared in the template
def inline = '''\
modelTypes = {
    List<Plant> plants
}
ul { plants.each { p -> li(p.nme) } }
'''
try { engine.createTemplate(inline) } catch (e) { println 'inline: ' + e.message.readLines().find { it.contains('nme') && it.contains('No such') } }
println engine.createTemplate(inline.replace('nme', 'name')).make(plants: plants)
<ul><li>tomato in bench-1</li></ul>
render: MissingPropertyException: No such property: nme for class: Plant
compile: MultipleCompilationErrorsException: GeneratedMarkupTemplate2: 1: [Static type checking] - No such property: nme for class: Plant
inline: GeneratedMarkupTemplate3: 4: [Static type checking] - No such property: nme for class: Plant
<ul><li>tomato</li></ul>

The static checking used here is the one described on Static Type Checking & @CompileStatic.

Other solutions

Groovy can drive any JVM template library. The documentation names FreeMarker, Velocity and StringTemplate as examples; they bring their own syntax and Maven coordinates and none of them is part of Groovy. Groovy also serves templated pages in the form of Groovlets and TemplateServlet, described on Groovlets, Servlets & JMX.

Where the book is outdated

The Manning book Groovy in Action (publisher page in the references) predates the MarkupTemplateEngine, so it cannot describe it. Check these points against the current documentation:

  • MarkupTemplateEngine (with type-checked models, layouts, fragments and autoEscape) is the engine to choose for HTML and XML output; the older engines are text substitution only.

  • StreamingTemplateEngine exists to lift the 64k string limit of SimpleTemplateEngine, which the run above reproduced.

  • The engines live in the separate groovy-templates module (org.apache.groovy:groovy-templates), not in the core jar.

  • The Groovy 6.0 documentation’s claim that XmlTemplateEngine escapes special characters did not hold for interpolated values in the run above.