JJTemplate is a lightweight templating engine designed for minimal render time and JSON-compatible input/output. It compiles templates into optimized abstract syntax trees (ASTs) for fast execution while guaranteeing valid JSON results.
IDEA plugin: here.
<dependency>
<groupId>io.github.sibmaks.jjtemplate</groupId>
<artifactId>jjtemplate</artifactId>
<version>0.7.0</version>
<type>pom</type>
</dependency>implementation("io.github.sibmaks.jjtemplate:jjtemplate:0.7.0")- Compile a template
var compiler = TemplateCompiler.getInstance(); var script = new TemplateScript(...); var compiled = compiler.compile(script);
- Render with data
var result = compiled.render(Map.of("name", "Alice"));
Templates are written in pure JSON with embedded expressions using double curly braces:
{
"definitions": [
{
"greeting": "{{ string:concat 'Hello, ', .name }}"
}
],
"template": {
"message": "{{ .greeting }}"
}
}-
.varName— access variable values -
{{ expression }}— direct expression substitution -
{{? expression }}— conditional insertion (skips if null) -
{{. expression }}— spread values into arrays or objects
Supports expressions, pipe calls (|), and ternary operators (?, :), function argument spread (...).
.varName- Access variable values from context- Supports nested object access (e.g.,
.user.profile.name)
Variable definitions: static, conditional (switch), and range-based (range)
Functions are organized into namespaces by type or purpose.
Call syntax uses a colon (:), e.g. {{ cast:str .value }} or {{ .text | string:upper }}.
cast:str(value)— Convert to stringcast:int(value)— Convert to integer (BigInteger)cast:float(value)— Convert to decimal (BigDecimal)cast:boolean(value)— Convert to boolean
string:concat(base, ...values)— Concatenate stringsstring:join(glue, ...values)— Concatenate strings with glue between valuesstring:joinNotEmpty(glue, ...values)— Concatenate strings with glue between values, skip null and empty valuesstring:len(string)— Get string lengthstring:empty(string)— Check if empty or nullstring:contains(string, ...substrings)— Check if all substrings exist in stringstring:format([locale], pattern, ...args)— Format string (likeString.format)string:lower([locale], value)— Convert to lowercasestring:upper([locale], value)— Convert to uppercasestring:trim(value)— Remove all leading and trailing spacestring:split(value, regex, [limit])— Splits this string around matches of the given regular expression.string:indexOf(value, str)— Returns the index within this string of the first occurrence of the specified substring.string:lastIndexOf(value, str)— Returns the index within this string of the last occurrence of the specified substring.string:substr(value, beginIndex, [endIndex])— Returns a string that is a substring of this string. Support negative indexes.string:replace(value, target, replacement)— Replaces each substring of this string that matches the literal target sequence with the specified literal replacement sequence.string:replaceAll(value, regex, replacement)— Replaces each substring of this string that matches the given regular expression with the given replacement. The substring begins at the specifiedbeginIndexand extends to the character at indexendIndex - 1.
list:new(...items)— Create a listlist:concat(...lists)— Concatenate multiple lists or arrayslist:len(list)— Get sizelist:empty(list)— Check if emptylist:contains(list, ...values)— Check if list contains all valueslist:head(list)— Get head of list or nulllist:tail(list)— Get tail of list or empty listlist:join(glue, ...lists)— Join all lists into single string
map:new(key, value, ...)— Create a mapmap:len(map)— Get number of entriesmap:empty(map)— Check if emptymap:contains(map, ...keys)— Check if all keys existmap:collapse(object|array|collection)— Merge object properties into one map
date:format([locale], pattern, date)— Format date (Date,LocalDate,LocalDateTime,ZonedLocalDateTime)date:parse(pattern, string)— Parse string intoLocalDatedate:now()— Get currentLocalDate
datetime:parse(pattern, string)— Parse string intoLocalDateTimedatetime:now()— Get currentLocalDateTime
locale:new(language[, country[, variant]])— Create aLocaleinstance
numberFormat:new(locale[, settings])— Create aNumberFormatinstance for the specifiedLocaleand optional settingsMap. Supportedsettingskeys:
style(number|integer|currency|percent),groupingUsed,parseIntegerOnly,maximumIntegerDigits,minimumIntegerDigits,maximumFractionDigits,minimumFractionDigits,currency,roundingMode.
math:neg(value)— Negate numeric valuemath:sum(left, right)— Sum two numeric valuesmath:sub(left, right)— Subtract two numeric valuesmath:mul(left, right)— Multiply two numeric valuesmath:div(left, right, [mode])— Divide two numeric values and scale using passedmode.math:scale(value, amount, mode)— Returns afloatwhose scale is the specified value, and whose unscaled value is determined by multiplying or dividing thisfloat's unscaled value by the appropriate power of ten to maintain its overall value.
default(value, fallback)— Return fallback if value isnull
(These remain global, without namespace.)
not(value)— Boolean inversioneq(a, b),neq(a, b)— Equality checkslt(a, b),le(a, b),gt(a, b),ge(a, b)— Comparisonsand(a, b),or(a, b),xor(a, b)— Logical operations
-
All functions can be used in pipe form, e.g.
{ "upperName": "{{ .name | string:upper }}" } -
Namespace separation ensures no name collisions and improves clarity.
JJTemplate supports inline conditional expressions using the ternary operator:
condition ? valueIfTrue : valueIfFalse
The operator evaluates the condition and returns one of two values:
- If the condition is true, the expression before the colon (
:) is returned. - If the condition is false or
null, the expression after the colon is returned.
{
"status": "{{ eq .ge 18 ? 'adult' : 'minor' }}"
}If .age >= 18, the result will be:
{
"status": "adult"
}Otherwise:
{
"status": "minor"
}Both condition and results (valueIfTrue / valueIfFalse)
can contain any expression, including function calls and pipes:
{
"greeting": "{{ .isMorning ? string:upper 'good morning' : string:upper 'good evening' }}"
}or with pipe syntax:
{
"formatted": "{{ .amount | gt 1000 ? 'large' : 'small' }}"
}Ternary expressions can be nested for compact logic:
{
"label": "{{ eq .type 'a' ? 'Alpha' : eq .type 'b' ? 'Beta' : 'Other' }}"
}See more examples here.
JJTemplate is built with a modular architecture:
- Lexer - Tokenizes template strings
- Parser - Constructs AST from tokens
- Compiler - Generates executable node trees
- Optimizer - Applies performance optimizations
- Runtime - Executes templates and produces output
- Minimal render time through AST optimization
- Clean separation of parsing, compilation, and execution
- Predictable output with JSON compatibility guarantees
- Optimized performance at every processing stage