Skip to content

Latest commit

 

History

287 Commits

Folders and files

NameName
Last commit message
Last commit date
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 

Repository files navigation

JJTemplate

English | Русский

Java GitHub Release Maven

Quality Gate Status Bugs Code Smells Coverage

Build Javadocs License

JJTemplate is a lightweight templating engine designed for minimal render time and JSON-compatible input/output. JJT stands for Java JSON Template. JJTemplate compiles templates into optimized abstract syntax trees (ASTs) for fast execution while guaranteeing valid JSON results.

IDEA plugin: here.

Usage

Maven

<dependency>
    <groupId>io.github.sibmaks.jjtemplate</groupId>
    <artifactId>jjtemplate</artifactId>
    <version>{version}</version>
    <type>pom</type>
</dependency>

Gradle

implementation("io.github.sibmaks.jjtemplate:jjtemplate:{version}")

Complete Example

The following class can be copied and run as-is:

import io.github.sibmaks.jjtemplate.compiler.api.TemplateCompiler;
import io.github.sibmaks.jjtemplate.compiler.api.TemplateScript;

import java.util.Map;

public class Main {
    public static void main(String[] args) {
        var script = TemplateScript.builder()
                .template(Map.of(
                        "message", "{{ string:concat 'Hello, ', .name }}"
                ))
                .build();

        var compiler = TemplateCompiler.getInstance();
        var compiled = compiler.compile(script);
        var result = compiled.render(Map.of("name", "Alice"));

        System.out.println(result); // {message=Hello, Alice}
    }
}

Custom Functions

Implement TemplateFunction and define the namespace and name used in JJT expressions. For example, the following function is available as custom:reverse:

import io.github.sibmaks.jjtemplate.compiler.runtime.fun.TemplateFunction;

import java.util.List;

public final class ReverseTemplateFunction implements TemplateFunction<String> {
    @Override
    public String invoke(List<Object> args, Object pipeArg) {
        if (!args.isEmpty()) {
            throw fail("no arguments expected after the pipe value");
        }
        return reverse(pipeArg);
    }

    @Override
    public String invoke(List<Object> args) {
        if (args.size() != 1) {
            throw fail("exactly 1 argument required");
        }
        return reverse(args.get(0));
    }

    @Override
    public String getNamespace() {
        return "custom";
    }

    @Override
    public String getName() {
        return "reverse";
    }

    @Override
    public boolean isDynamic() {
        return false;
    }

    private String reverse(Object value) {
        return value == null ? null : new StringBuilder(value.toString()).reverse().toString();
    }
}

Register the function in the evaluation options and create the compiler with those options:

var evaluationOptions = TemplateEvaluationOptions.builder()
        .functions(List.of(new ReverseTemplateFunction()))
        .build();
var compileOptions = TemplateCompileOptions.builder()
        .evaluationOptions(evaluationOptions)
        .build();
var compiler = TemplateCompiler.getInstance(compileOptions);

The function can then be called directly or through a pipe:

{
  "direct": "{{ custom:reverse .value }}",
  "pipe": "{{ .value | custom:reverse }}"
}

Template Format

Templates are written in pure JSON with embedded expressions using double curly braces:

{
  "definitions": [
    {
      "greeting": "{{ string:concat 'Hello, ', .name }}"
    }
  ],
  "template": {
    "message": "{{ .greeting }}"
  }
}

Expression Types

  • .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 (...).

Core Concepts

Variables and Access

  • .varName - Access variable values from context
  • Supports nested object access (e.g., .user.profile.name)

Variable definitions: static, conditional (switch), and range-based (range). A range exposes item,index for collections and arrays, and key,value for maps.

Built-in Functions

Functions are organized into namespaces by type or purpose. Call syntax uses a colon (:), e.g. {{ cast:str .value }} or {{ .text | string:upper }}.


cast — Type Conversions

  • cast:str(value) — Convert to string
  • cast:int(value) — Convert to integer (BigInteger)
  • cast:float(value) — Convert to decimal (BigDecimal)
  • cast:boolean(value) — Convert to boolean

string — String Operations

  • string:concat(base, ...values) — Concatenate strings
  • string:join(glue, ...values) — Concatenate strings with glue between values
  • string:joinNotEmpty(glue, ...values) — Concatenate strings with glue between values, skip null and empty values
  • string:len(string) — Get string length
  • string:empty(string) — Check if empty or null
  • string:contains(string, ...substrings) — Check if all substrings exist in string
  • string:format([locale], pattern, ...args) — Format string (like String.format)
  • string:lower([locale], value) — Convert to lowercase
  • string:upper([locale], value) — Convert to uppercase
  • string:trim(value) — Remove all leading and trailing space
  • string: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 specified beginIndex and extends to the character at index endIndex - 1.

list — List / Array Operations

  • list:new(...items) — Create a list
  • list:concat(...lists) — Concatenate multiple lists or arrays
  • list:len(list) — Get size
  • list:empty(list) — Check if empty
  • list:contains(list, ...values) — Check if list contains all values
  • list:head(list) — Get head of list or null
  • list:tail(list) — Get tail of list or empty list
  • list:join(glue, ...lists) — Join all lists into single string

map — Map / Object Operations

  • map:new(key, value, ...) — Create a map
  • map:len(map) — Get number of entries
  • map:empty(map) — Check if empty
  • map:contains(map, ...keys) — Check if all keys exist
  • map:collapse(object|array|collection) — Merge object properties into one map

date — Date Utilities

  • date:format([locale], pattern, date) — Format date (Date, GregorianCalendar, LocalDate, LocalDateTime, ZonedLocalDateTime)
  • date:parse(pattern, string) — Parse string into LocalDate
  • date:now() — Get current LocalDate

datetime — DateTime Utilities

  • datetime:parse(pattern, string) — Parse string into LocalDateTime
  • datetime:now() — Get current LocalDateTime

locale — Locale Utilities

  • locale:new(language[, country[, variant]]) — Create a Locale instance

numberFormat — Number Format Utilities

  • numberFormat:new(locale[, settings]) — Create a NumberFormat instance for the specified Locale and optional settings Map. Supported settings keys:
  • style (number|integer|currency|percent),
  • groupingUsed,
  • parseIntegerOnly,
  • maximumIntegerDigits,
  • minimumIntegerDigits,
  • maximumFractionDigits,
  • minimumFractionDigits,
  • currency,
  • roundingMode.

math — Math Operations

  • math:neg(value) — Negate numeric value
  • math:sum(left, right) — Sum two numeric values
  • math:sub(left, right) — Subtract two numeric values
  • math:mul(left, right) — Multiply two numeric values
  • math:div(left, right, [mode]) — Divide two numeric values and scale using passed mode.
  • math:scale(value, amount, mode) — Returns a float whose scale is the specified value, and whose unscaled value is determined by multiplying or dividing this float's unscaled value by the appropriate power of ten to maintain its overall value.

default

  • default(value, fallback) — Return fallback if value is null

Safe Member Access

Use ?. when a property may not exist on the runtime object. A missing property is resolved as null, so it can be combined with default:

{
  "on": "{{ default .repository?.on, false }}"
}

For Java beans, ?.on resolves either a public on field or a zero-argument getOn() / isOn() accessor. The same operator safely calls methods:

{
  "value": "{{ default .repository?.foo('bar'), false }}"
}

If no method matches the supplied arguments, the call resolves as null. Safe access does not hide exceptions thrown by an existing property accessor or a matching method.


Logical and Comparison Operators

(These remain global, without namespace.)

  • not(value) — Boolean inversion
  • eq(a, b), neq(a, b) — Equality checks
  • lt(a, b), le(a, b), gt(a, b), ge(a, b) — Comparisons
  • and(a, b), or(a, b), xor(a, b) — Logical operations

Notes

  • All functions can be used in pipe form, e.g.

    { "upperName": "{{ .name | string:upper }}" }
  • Namespace separation ensures no name collisions and improves clarity.

  • default, and, and or evaluate their arguments lazily. default evaluates its fallback only when the input is null; and and or use boolean short-circuit evaluation.

  • Custom TemplateFunction implementations can opt into the same behavior by overriding isLazy() and accessing only the arguments they need from the supplied List.


? Conditional Expressions (Ternary Operator)

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.

Example

{
  "status": "{{ eq .ge 18 ? 'adult' : 'minor' }}"
}

If .age >= 18, the result will be:

{
  "status": "adult"
}

Otherwise:

{
  "status": "minor"
}

Expressions inside ternary

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' }}"
}

Nesting

Ternary expressions can be nested for compact logic:

{
  "label": "{{ eq .type 'a' ? 'Alpha' : eq .type 'b' ? 'Beta' : 'Other' }}"
}

See more examples here.

Architecture

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

Goals

  • 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

About

Java JSON Template. Library to transform template as valid JSON into JSON

Resources

Stars

1 star

Watchers

0 watching

Forks

Releases

Packages

Contributors

Languages