Text Blocks and New String APIs (Java 15+)
1. Overview
Before Java 15, writing a multi-line JSON, SQL, or HTML snippet in Java code was tedious. You had to concatenate strings with +, add \n characters, and escape every " mark. Text blocks (officially from Java 15, JEP 378) solve this problem. Along with them come several new methods in the String class.
After reading this article, you’ll write text blocks with correct indentation and use the \ and \s escape sequences. You’ll also know when to use formatted(), stripIndent(), translateEscapes(), and several other APIs added up to Java 21.
2. Setup
All examples run on Java 21. The test section uses JUnit 5 and AssertJ:
<properties>
<maven.compiler.release>21</maven.compiler.release>
</properties>
<dependencies>
<dependency>
<groupId>org.junit.jupiter</groupId>
<artifactId>junit-jupiter</artifactId>
<version>5.10.2</version>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.assertj</groupId>
<artifactId>assertj-core</artifactId>
<version>3.25.3</version>
<scope>test</scope>
</dependency>
</dependencies>
If you use Spring Boot 3.x, spring-boot-starter-test already includes both libraries.
3. Text Blocks Basics
3.1. Syntax
A text block opens with three double quotes """ followed immediately by a line break. It closes with """:
public class TextBlockDemo {
public static void main(String[] args) {
String json = """
{
"id": 42,
"customer": "Lan",
"status": "PAID"
}
""";
System.out.print(json);
}
}
Output:
{
"id": 42,
"customer": "Lan",
"status": "PAID"
}
This code snippet has several noteworthy points:
- Double quotes inside don’t need escaping.
- Common indentation at the start of each line is stripped. This is called incidental whitespace. Relative indentation, such as 2 spaces before
"id", is preserved. - Because the closing
"""is on its own line, the string ends with\n.
A text block is an ordinary String. It’s processed entirely at compile time, so it incurs no extra runtime cost.
Note: You cannot write content immediately after the opening
""". The lineString s = """Hello""";will produce a compile error: illegal text block open delimiter sequence.
3.2. The Position of the Closing """ Determines Indentation
The compiler finds the line with the least leading whitespace, including the line containing the closing """. It then strips exactly that number of leading characters from all lines. Therefore, you can intentionally add indentation by moving the closing """ to the left:
String html = """
<ul>
<li>Order #1</li>
</ul>
""";
System.out.print(html);
<ul>
<li>Order #1</li>
</ul>
The closing """ has 4 fewer columns of indentation than the content, so each line retains 4 leading spaces.
But if you don’t want a \n character at the end of the string, place """ immediately after the content:
String greeting = """
Hello Lan""";
System.out.println("[" + greeting + "]");
[Hello Lan]
3.3. Trailing Whitespace and Line Termination
There are two other rules you need to know:
- Trailing whitespace at the end of each line is always stripped.
- All line termination styles (
\r\n,\r) are normalized to\n. This holds even if the source file uses Windows line endings.
Rule 2 ensures consistent results across platforms. However, if your protocol requires \r\n, such as for HTTP headers or CSV files per RFC 4180, you must manually replace the line terminator.
4. Two New Escape Sequences: \<line-terminator> and \s
4.1. Line Joining with \
A long SQL statement is often split across multiple lines for readability. But sometimes you need it on a single line, such as in a log message. A backslash \ at the end of a line suppresses the line terminator:
String sql = """
SELECT id, total \
FROM orders \
WHERE status = 'PAID'""";
System.out.println(sql);
SELECT id, total FROM orders WHERE status = 'PAID'
Spaces before the \ are preserved. The reason is that they’re no longer at the end of the line—the backslash comes after them.
4.2. Preserving Whitespace with \s
\s is equivalent to a space character (U+0020). Use it to preserve trailing whitespace that would normally be stripped, such as when creating fixed-width data:
String report = """
Lan \s
Minh \s
""";
report.lines().forEach(line -> System.out.println("[" + line + "]"));
[Lan ]
[Minh ]
Each line is exactly 8 characters long. The four spaces before \s are preserved because they’re no longer at the end of the line.
4.3. When Content Contains """
Two consecutive double quotes are fine. Three consecutive quotes would be interpreted as the text block terminator, so you need to escape at least one of them:
String doc = """
Python docstrings use \""" to open and close.
""";
5. New String APIs
Java 15 added three methods to String to support text blocks. These are also the steps the compiler performs when processing a text block.
5.1. formatted()
formatted(Object... args) works like String.format(this, args), but you call it directly on the string. This style is especially suited to text blocks:
record Order(long id, String customer, double total) {}
Order order = new Order(42, "Lan", 199_000);
String message = """
Order #%d
Customer: %s
Total: %,.0f VND
""".formatted(order.id(), order.customer(), order.total());
System.out.print(message);
Order #42
Customer: Lan
Total: 199,000 VND
The thousands separator depends on the JVM’s default Locale. For a fixed result, use String.format(Locale.US, ...), since formatted() doesn’t accept a Locale parameter.
5.2. stripIndent()
This method applies the exact incidental whitespace-stripping algorithm of text blocks to any string. It’s useful when the multi-line string comes from a file or database rather than source code:
String raw = " SELECT *\n FROM customers\n ";
System.out.print(raw.stripIndent());
SELECT *
FROM customers
The last line contains only whitespace, so it’s included in determining the indentation and becomes an empty string. This is exactly the role of the closing """ in a text block.
5.3. translateEscapes()
This method converts literal escape sequences into their corresponding characters. You’ll need it when reading templates from configuration files that contain strings like \n or \t:
String fromConfig = "Hello\\tLan\\nThank you for placing an order";
System.out.println(fromConfig.translateEscapes());
Hello Lan
Thank you for placing an order
Malformed escape sequences, such as \q, will cause the method to throw IllegalArgumentException. Don’t call it on user-supplied data without catching the exception. Also note: this method doesn’t handle \u sequences because Unicode escapes are already processed by the compiler.
6. Other Useful Methods (Java 11–21)
Many small methods have been added across versions. They often work together with text blocks:
String text = """
Lan
\s
Minh
""";
// Java 11
System.out.println(" ".isBlank()); // true
System.out.println(text.lines().filter(l -> !l.isBlank()).count()); // 2
System.out.println("\u2003Lan\u2003".strip()); // "Lan" (strip understands Unicode, trim doesn't)
System.out.println("-".repeat(10)); // ----------
// Java 12
System.out.print("Lan\nMinh".indent(2)); // add 2 spaces per line, ends with \n
int length = "Lan".transform(String::length); // 3
// Java 21
System.out.println("order-42-paid".indexOf('-', 6, 13)); // 8, search only in [6, 13)
System.out.println(Arrays.toString("a,b;c".splitWithDelimiters("[,;]", 0)));
// [a, , , b, ;, c]
A few details worth clarifying:
indent(n)adds n spaces to the start of each line ifn > 0, or removes up to n leading spaces ifn < 0. It always normalizes line terminators and adds\nat the end.transform(Function)lets you chain string transformations fluently, for exampletemplate.transform(this::render).transform(String::strip).- From Java 21,
StringBuilderalso hasrepeat(...):new StringBuilder().repeat("=", 5)produces=====.
7. Using Text Blocks in Spring Boot
In Spring Boot 3.x applications, text blocks work great with Spring Data JPA’s @Query. You can keep multi-line queries readable without string concatenation:
public interface OrderRepository extends JpaRepository<Order, Long> {
@Query("""
SELECT o FROM Order o
WHERE o.customer.email = :email
AND o.status = 'PAID'
ORDER BY o.createdAt DESC
""")
List<Order> findPaidOrders(@Param("email") String email);
}
Text blocks are compile-time constants, so they work as annotation values. Parameters are still bound via :email, protecting against SQL injection.
8. Writing Tests for Text Block Behavior
The rules about indentation and line terminators can be confusing. So you should write tests to pin down the behavior you expect:
import org.junit.jupiter.api.Test;
import static org.assertj.core.api.Assertions.assertThat;
class TextBlockTest {
@Test
void givenClosingDelimiterOnOwnLine_thenEndsWithNewline() {
String json = """
{
"id": 42
}
""";
assertThat(json).isEqualTo("{\n \"id\": 42\n}\n");
}
@Test
void givenBackslashAtLineEnd_thenLinesAreJoined() {
String sql = """
SELECT id \
FROM orders""";
assertThat(sql).isEqualTo("SELECT id FROM orders");
}
@Test
void givenSpaceEscape_thenTrailingSpacesArePreserved() {
String padded = """
Lan \s""";
assertThat(padded).hasSize(6).endsWith(" ");
}
@Test
void givenTemplate_whenFormatted_thenPlaceholdersAreReplaced() {
String message = """
Hello %s, order #%d has been paid.""".formatted("Lan", 42);
assertThat(message).isEqualTo("Hello Lan, order #42 has been paid.");
}
}
All four tests pass. The first one is worth keeping above all: it reminds you that a closing """ on its own line creates a \n at the end of the string.
9. Common Mistakes and When Not to Use Text Blocks
Mixing tabs and spaces. When calculating incidental whitespace, the compiler treats one tab as one whitespace character, just like a space. If lines are indented with two different styles, the result will be off, even if they look aligned in your IDE. Configure your editor to use only spaces.
Forgetting the trailing \n. When comparing strings or concatenating a text block with another string, an extra \n easily causes errors like “looks identical but equals returns false.” If you don’t need it, place """ right after the content or call strip().
Using formatted() to build SQL with user data. Text blocks only make code more readable; they don’t make it safer.
// ⚠️ UNSAFE: SQL injection vulnerability
String sql = """
SELECT * FROM customers WHERE email = '%s'
""".formatted(email);
Use PreparedStatement, JdbcTemplate with ? parameters, or @Query with named parameters instead.
Note: For JSON, text blocks are fine for test data or static payloads. If the payload depends on dynamic data, use Jackson to serialize objects. Manual string concatenation won’t escape special characters like
"or\in customer names.
Single-line short strings. "Lan" is still better than a three-line text block. Use text blocks only when the content truly spans multiple lines or contains many " characters.
Overly large templates. A multi-hundred-line HTML email embedded in a Java class makes the code hard to maintain. Move it to a template file instead, such as using Thymeleaf or a resource in src/main/resources.
10. Conclusion
Text blocks let you write JSON, SQL, and HTML in Java as cleanly and readably as in their native form. You just need to remember a few rules:
- The opening
"""must be followed immediately by a line break. - The position of the closing
"""determines indentation and whether the string ends with\n. - Trailing whitespace is stripped. Line terminators are always normalized to
\n. - Use
\to join lines and\sto preserve whitespace.
Regarding the API, formatted() is excellent for filling values into text blocks. stripIndent() and translateEscapes() let you apply the same logic to strings read from external sources. Methods like lines(), strip(), indent(), transform(), and splitWithDelimiters() from Java 21 help you manipulate multi-line strings without external libraries. Finally, write tests to pin down the behavior you need, and don’t use text blocks to manually concatenate SQL with user data.