Software Engineer · Java · Spring Boot · Microservices
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
- The Real Problem: String Handling in Java Has Always Been Dangerous
- Java Text Blocks (Available Since Java 15)
- Java String Templates (JEP 430, Preview in Java 21–22)
- Custom Template Processors: The Security Game-Changer
- HTML and JSON Processors
- Text Blocks vs String Templates: When to Use Which
- Failure Scenarios and Gotchas
- Production Best Practices
- Key Takeaways
- 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.
'; 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:
- String concatenation (
+): Verbose at scale, produces unreadable escaped literals for multi-line content, and is completely transparent to any injection-awareness layer. String.format(): More readable for simple templates but still not type-safe for SQL or HTML context —%sinserts raw user content with no escaping whatsoever.MessageFormat: Designed for internationalized UI strings, not for dynamic code generation. Its{0}placeholders provide no injection protection and its escaping rules are non-obvious.StringBuilder: Efficient for high-frequency loops but even harder to audit for injection because the string assembly is spread across manyappend()calls.
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.
\{}) 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("&", "&")
.replace("<", "<")
.replace(">", ">")
.replace("\"", """)
.replace("'", "'");
}
}
// Usage — XSS neutralised automatically
String userComment = "<script>alert('xss')</script>";
String safeHtml = HTML."<p class=\"comment\">\{userComment}</p>";
// Output: <p class="comment"><script>alert('xss')</script></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
- String concatenation and
String.format()are context-blind — they assemble strings without any awareness of the target system, making injection vulnerabilities a developer-discipline problem rather than a structural impossibility. - Java Text Blocks (Java 15, stable) solve the readability problem — multi-line SQL, JSON, and HTML become maintainable, diff-able, and reviewable without escape sequences or string concatenation.
- Custom String Template processors make injection structurally impossible — a well-written SQL processor cannot produce an un-parameterized query regardless of user input, eliminating an entire vulnerability class at the language level.
- JEP 430 was withdrawn from Java 23+ for redesign — teams targeting Java 21/22 LTS can use it with
--enable-previewbut should plan for API changes; for Java 23+ wait for the redesigned feature or use PreparedStatement wrappers. - Text blocks and String Templates compose — use text blocks for multi-line structure and custom template processors for the dynamic, user-supplied values that carry injection risk; together they cover all practical string-handling scenarios cleanly.
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 PostDiscussion / Comments
Related Posts
Java Record Patterns
Leverage record patterns and pattern matching for switch to write expressive, type-safe Java code.
Java Scoped Values
Replace ThreadLocal with scoped values for safe, immutable context propagation in virtual-thread applications.
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