Java String Templates JEP 430 and Text Blocks — safe dynamic strings in Java 21+
Md Sanwar Hossain - Software Engineer
Md Sanwar Hossain

Software Engineer · Java · Spring Boot · Microservices

Core Java March 21, 2026 13 min read Java Performance Engineering Series

Java String Templates (JEP 430) and Text Blocks: Safe Dynamic Strings Without the Security Nightmares

String handling in Java has always been a minefield of escaped characters, verbose concatenation, and — in the wrong hands — catastrophic injection vulnerabilities. Java Text Blocks (stable since Java 15) solved the multi-line readability problem. Java String Templates (JEP 430, previewed in Java 21–22) go further: they introduce structured interpolation with custom processors that can sanitize values at the language level, making entire vulnerability classes structurally impossible. This guide covers both features, their security implications, and when to use each.

Table of Contents

  1. The Real Problem: String Handling in Java Has Always Been Dangerous
  2. Java Text Blocks (Available Since Java 15)
  3. Java String Templates (JEP 430, Preview in Java 21–22)
  4. Custom Template Processors: The Security Game-Changer
  5. HTML and JSON Processors
  6. Text Blocks vs String Templates: When to Use Which
  7. Failure Scenarios and Gotchas
  8. Production Best Practices
  9. Key Takeaways
  10. Conclusion

1. The Real Problem: String Handling in Java Has Always Been Dangerous

A fintech startup's Spring Boot API began triggering SQL injection alerts in their SAST scanner. The culprit was a reporting service where developers had been building dynamic SQL strings with concatenation. They'd been told repeatedly to "use PreparedStatement," but the team kept "temporarily" using string concatenation for complex dynamic queries — filter clauses with a variable number of conditions, ORDER BY fields derived from user input, and multi-tenant schema-name prefixes. "Temporary" lasted eighteen months.

2025 incident: A penetration tester submitted a crafted report parameter containing '; DROP TABLE audit_log; --. The reporting service's dynamic ORDER BY clause was assembled via string concatenation and passed directly to JDBC without parameterization. The injection succeeded in staging. It would have succeeded in production. The fix required a six-week refactor of forty-seven query-building methods. Java String Templates with a custom SQL processor would have made this category of vulnerability structurally unreachable from day one.

Java has offered several string-building mechanisms over its history, and each has characteristic failure modes:

None of these mechanisms know what kind of string they are building. A SQL processor that understands it is constructing a query can enforce parameterization. A raw + operator cannot. This is the gap that Java String Templates close.

2. Java Text Blocks (Available Since Java 15)

Text Blocks landed as a standard feature in Java 15 (JEP 378) and solve the readability problem for multi-line string literals. They eliminate the escape sequences and manual newline characters that made JSON, SQL, and HTML embedded in Java source code nearly unmaintainable.

// Before text blocks — the escape nightmare
String json = "{\n  \"name\": \"" + user.getName() + "\",\n  \"id\": " + user.getId() + "\n}";

// With text blocks — clean and readable
String json = """
        {
          "name": "%s",
          "id": %d
        }
        """.formatted(user.getName(), user.getId());

// SQL query text block — repository-friendly and diff-able
String sql = """
        SELECT u.id, u.email, o.total
        FROM users u
        JOIN orders o ON u.id = o.user_id
        WHERE u.created_at > :since
          AND o.status = :status
        ORDER BY o.created_at DESC
        LIMIT :limit
        """;

Text blocks use incidental whitespace trimming: the common leading whitespace determined by the position of the closing """ delimiter is stripped from every line, so indentation in your source file does not bleed into the runtime string. The .formatted() method chains cleanly for simple variable substitution.

For SQL in particular, text blocks deliver immediate value: queries stored as text block fields in repository classes are readable in code review, render correctly in IDE SQL inspectors, and are trivially extracted into .sql files if your codebase later adopts a SQL migration tool. They are production-ready today and require no preview flags.

3. Java String Templates (JEP 430, Preview in Java 21–22)

String Templates (JEP 430) introduced a fundamentally new kind of expression: a template expression consisting of a processor and a template literal containing embedded \{expression} slots. The processor receives both the literal fragments and the evaluated values as separate lists, giving it full control over how the final result is assembled.

// STR processor — basic interpolation (Java 21 preview)
String name = "World";
String greeting = STR."Hello, \{name}!";
// Output: "Hello, World!"

// Multi-line with STR
String report = STR."""
        User Report:
          Name:   \{user.getName()}
          Email:  \{user.getEmail()}
          Orders: \{user.getOrderCount()}
          Since:  \{user.getCreatedAt().format(DateTimeFormatter.ISO_DATE)}
        """;

// Full Java expressions are valid inside \{}
String status = STR."Order \{order.getId()} is \{order.isPaid() ? "PAID" : "PENDING"}";

The built-in STR processor performs simple concatenation — equivalent to + but syntactically cleaner. A second built-in processor, FMT, supports printf-style format specifiers: FMT."Balance: %,.2f\{amount}". Neither processor provides injection safety on its own. The safety comes from writing a custom processor.

Key insight: The interpolation slots (\{}) and the literal fragments are kept separate all the way through to the processor. The processor is the only place where they are combined. This separation is what makes context-aware escaping and parameterization possible at the language level — not just as a convention or library discipline.

4. Custom Template Processors: The Security Game-Changer

Using the STR processor for SQL or HTML is just as dangerous as string concatenation — it performs the same dumb substitution. The transformative capability is implementing StringTemplate.Processor yourself. A custom SQL processor can enforce parameterization unconditionally: literal fragments become the query skeleton, interpolated values always become JDBC parameters. There is no way to accidentally bypass this because the processor is the only path to a result.

// Custom SQL template processor — injection-safe by design
public class SafeSQL implements StringTemplate.Processor<PreparedStatement, SQLException> {
    private final Connection connection;

    public SafeSQL(Connection connection) {
        this.connection = connection;
    }

    @Override
    public PreparedStatement process(StringTemplate template) throws SQLException {
        // fragments() = literal parts between \{} slots
        // values()    = evaluated expressions from each \{} slot
        StringBuilder sql = new StringBuilder();
        List<Object> params = new ArrayList<>();
        List<String> fragments = template.fragments();
        List<Object> values = template.values();

        for (int i = 0; i < fragments.size(); i++) {
            sql.append(fragments.get(i));
            if (i < values.size()) {
                sql.append('?');        // Always parameterize — never inline
                params.add(values.get(i));
            }
        }

        PreparedStatement stmt = connection.prepareStatement(sql.toString());
        for (int i = 0; i < params.size(); i++) {
            stmt.setObject(i + 1, params.get(i));
        }
        return stmt;
    }
}

// Usage — SQL injection impossible by construction
SafeSQL SQL = new SafeSQL(connection);
String userInput = "'; DROP TABLE users; --";  // attacker-controlled input
PreparedStatement ps = SQL."SELECT * FROM users WHERE name = \{userInput}";
// SQL executed: SELECT * FROM users WHERE name = ?
// Parameter:   "'; DROP TABLE users; --"  (harmless string value)
// The injection attempt is parameterized away automatically — no developer discipline required

Notice what this achieves architecturally: the developer cannot accidentally produce an injection-vulnerable query using this processor. The processor type system enforces it. The attack string from the fintech incident above becomes a harmless bound parameter regardless of its content. This is fundamentally different from code review or SAST scanning — those are detective controls. A typed template processor is a preventive control baked into the language.

5. HTML and JSON Processors

The same pattern extends naturally to HTML and JSON generation. An HTML processor that HTML-escapes every interpolated value eliminates XSS at the template layer:

// HTML-escaping template processor
public class HTMLProcessor implements StringTemplate.Processor<String, RuntimeException> {
    public static final HTMLProcessor HTML = new HTMLProcessor();

    @Override
    public String process(StringTemplate template) {
        StringBuilder sb = new StringBuilder();
        List<String> fragments = template.fragments();
        List<Object> values = template.values();
        for (int i = 0; i < fragments.size(); i++) {
            sb.append(fragments.get(i));           // literal HTML — trusted
            if (i < values.size()) {
                sb.append(htmlEscape(String.valueOf(values.get(i))));
            }
        }
        return sb.toString();
    }

    private String htmlEscape(String input) {
        return input
            .replace("&", "&amp;")
            .replace("<", "&lt;")
            .replace(">", "&gt;")
            .replace("\"", "&quot;")
            .replace("'", "&#x27;");
    }
}

// Usage — XSS neutralised automatically
String userComment = "<script>alert('xss')</script>";
String safeHtml = HTML."<p class=\"comment\">\{userComment}</p>";
// Output: <p class="comment">&lt;script&gt;alert('xss')&lt;/script&gt;</p>

For JSON, a similar processor can serialize values using a Jackson ObjectMapper for structured types, or apply JSON-string escaping for raw strings. The key architectural property is always the same: the processor owns all interpolation and can apply context-specific escaping without any per-call developer action.

6. Text Blocks vs String Templates: When to Use Which

The two features solve different problems and are often used together rather than as alternatives:

Feature Text Blocks String Templates
Availability Java 15 (stable) Java 21–22 (preview; withdrawn from Java 23+)
Interpolation .formatted() only Native \{expression}
Security None built-in Custom processors enforce escaping/parameterization
Return type Always String Generic — processor decides (PreparedStatement, Document, etc.)
Best for Multi-line string literals, static SQL skeletons, JSON/XML fixtures Dynamic safe strings where injection prevention is required

In practice, use text blocks for the structure of your templates (multi-line SQL skeletons, HTML scaffolding) and a custom String Template processor for the dynamic variable substitution that introduces injection risk. They compose naturally: a text block can be the literal portion of a template expression.

7. Failure Scenarios and Gotchas

JEP 430 withdrawal from Java 23+. String Templates were withdrawn from the JEP list in September 2024 after the Java language team determined the design needed more iteration — specifically around the ergonomics of the processor API and how template expressions interact with type inference. As of Java 23 and 24, String Templates are not available even as a preview. Teams targeting Java 21 or 22 can still use them with --enable-preview, but they must plan for API changes when the feature eventually re-stabilises. For production Java 21+ LTS code, use text blocks plus custom wrapper classes built on PreparedStatement until the feature stabilises.

The STR processor is too permissive. One reason JEP 430 was withdrawn is that the built-in STR processor provides no safety guarantees and was considered too easy to misuse for SQL and HTML construction. If you adopt String Templates in preview mode, treat STR as a display/logging tool only, never for any string that will be interpreted by another system.

Text block indentation and tab vs. spaces. The incidental whitespace algorithm counts spaces; a single tab character counts as one space unit regardless of your editor's tab width setting. Mixing tabs and spaces in text block indentation produces surprising results. Configure your IDE to use spaces exclusively in files containing text blocks, and enforce this in your .editorconfig.

// Closing delimiter position controls indentation stripping
String correct = """
        line one
        line two
        """;   // trailing newline included; 8 spaces stripped from each line

String noTrailingNewline = """
        line one
        line two""";  // closing delimiter on same line as last content — no trailing newline

// Tab/space mixing — avoid this
String danger = """
\t    mixed indentation
        spaces only
        """;  // tab counted as 1 char — first line loses less whitespace than expected

Performance: String Templates vs StringBuilder vs format(). Microbenchmarks (JMH, Java 21) show the STR processor compiles to essentially the same bytecode as a StringBuilder chain — no runtime overhead. Custom processors add only the cost of their own logic (parameter binding, escaping). String.format() remains 3–5x slower than STR for equivalent outputs due to format-string parsing overhead. For hot loops generating thousands of strings per second, continue using StringBuilder directly; for all other code, readability wins.

8. Production Best Practices

Never use STR for SQL or HTML. Enforce this in code review checklists and SAST rules. The only legitimate uses for the built-in STR processor are log messages, display strings, and developer-facing output where no system will parse or execute the result. Every SQL, HTML, JSON, or shell-command string that involves user input must go through a sanitizing custom processor.

Use text blocks for all multi-line SQL in repositories. Store your named queries, native SQL strings, and JPQL in text block constants in repository classes or companion Queries interfaces. This makes queries reviewable in diffs, searchable via grep, and portable to external SQL files without rewriting. Pair with Spring Data's @Query or JDBI's @SqlQuery annotations for clean integration.

Combine with records for type-safe config generation. Java records plus a custom template processor create a powerful pattern for generating configuration files, infrastructure-as-code snippets, or API request bodies with compile-time safety:

// Record + text block for typed config generation
record DatabaseConfig(String host, int port, String dbName, String user) {
    String toJdbcUrl() {
        // Text block keeps multi-line format readable
        return """
                jdbc:postgresql://%s:%d/%s?user=%s&sslmode=require
                """.formatted(host, port, dbName, user).strip();
    }
}

// Usage
var cfg = new DatabaseConfig("db.prod.internal", 5432, "orders", "svc_orders");
System.out.println(cfg.toJdbcUrl());
// jdbc:postgresql://db.prod.internal:5432/orders?user=svc_orders&sslmode=require

IDE support and formatting. IntelliJ IDEA 2024.1+ provides full text block formatting, including auto-trimming of trailing spaces, smart indentation adjustment when you move the closing delimiter, and live preview of the runtime string value. For String Templates (where supported via preview), IntelliJ highlights \{} expressions with the same syntax colouring as regular Java expressions. Enable the "Java 21 Preview" language level in your project SDK settings to activate full IDE support.

Key Takeaways

Conclusion

The fintech team's SQL injection problem was not caused by ignorance — it was caused by the gap between what Java's string tools could enforce and what developers actually did under time pressure. Text blocks close the readability gap: there is no longer a reason to write multi-line SQL as escaped single-line strings. Custom String Template processors close the safety gap: there is no longer a reason to manually remember to parameterize every user-controlled value.

Together, they represent a significant step toward making Java's string handling as safe as it is powerful. The withdrawal of JEP 430 from Java 23+ is a temporary pause, not a cancellation — the Java team is investing in getting the API right before stabilising it. In the meantime, text blocks are stable and available today, and the preview version of String Templates in Java 21–22 is sufficient for teams willing to adopt a preview feature on a non-LTS release.

For a broader look at the modern Java feature landscape — records, sealed classes, pattern matching, and what's coming in future JEPs — see our Java Modern Features guide covering everything in Java 16 through 23 that changes how you write production Java code.

Read Full Blog Here

Explore the complete guide including live code examples, custom processor implementations, and production patterns for safe string handling in Java 21+.

Read the Full Post

Discussion / Comments

Related Posts

Core Java

Java Record Patterns

Leverage record patterns and pattern matching for switch to write expressive, type-safe Java code.

Core Java

Java Scoped Values

Replace ThreadLocal with scoped values for safe, immutable context propagation in virtual-thread applications.

Core Java

Java Structured Concurrency

Manage concurrent subtasks as a unit with structured concurrency — cleaner cancellation, error handling, and observability.

Last updated: March 2026 — Written by Md Sanwar Hossain