You have pasted JSON into a Java file and then spent five minutes turning it into a + chain: every quote escaped, every newline a \n, every line a fragment. The payload was readable in the ticket. It is not readable in the source.
Text blocks are the language’s answer for that shape of string.
A text block is a multiline String literal delimited by """. The compiler strips the indentation that exists only to line the literal up with your code. The result is still a String — same type, same intern pool, same APIs. Reach for them when the value looks like a document: JSON, SQL, HTML, a fixture — not when it is a one-liner or a regex you will regret indenting.
The concatenation soup
Before text blocks, a small JSON body looked like this (and yes, teams really shipped this):
String payload = "{\n"
+ " \"name\": \"Ada\",\n"
+ " \"role\": \"engineer\",\n"
+ " \"active\": true\n"
+ "}";
SQL was worse, because the query is the thing you need to read:
String sql = "SELECT u.id, u.email\n"
+ "FROM users u\n"
+ "JOIN accounts a ON a.user_id = u.id\n"
+ "WHERE u.status = 'ACTIVE'\n"
+ "ORDER BY u.email";
With a text block, the source looks like the document:
String payload = """
{
"name": "Ada",
"role": "engineer",
"active": true
}
""";
String sql = """
SELECT u.id, u.email
FROM users u
JOIN accounts a ON a.user_id = u.id
WHERE u.status = 'ACTIVE'
ORDER BY u.email
""";
No +. No \". Double quotes are ordinary characters. Newlines are the newlines you typed. The type of both variables is still String.
When it shipped
| Release | Status | Spec |
|---|---|---|
| Java 13 | First preview | JEP 355 |
| Java 14 | Second preview (\s, line-continuation \) | JEP 368 |
| Java 15 | Standard feature | JEP 378 |
| Java 17 LTS | Available on the LTS most teams run | — |
Use Java 15+. In practice most production codebases meet text blocks on Java 17 LTS (or newer). No preview flag is required once you are on 15+.
There is no interpolation syntax in the language. String templates were previewed later and then withdrawn — do not wait for STR. Pair a text block with .formatted(...) when you need placeholders.
Mental model
Three rules cover most of what you need:
- The opening
"""must be followed by a newline. Content starts on the next line, never on the delimiter line. - The closing
"""column is the indent ruler. Whitespace to the left of that column is incidental and stripped; whitespace further right stays in the string. - The value is an ordinary
String. You can pass it toequals, intern it, concatenate it, or hand it to an API that expectsString.
| You want | What to do |
|---|---|
| Open a text block | """ then a newline — nothing else on that line (spaces before the newline are ignored) |
| Control left margin | Move the closing """ |
| Keep a trailing newline | Put closing """ on its own line |
| Drop the trailing newline | Put closing """ at the end of the last content line |
| Substitute values | .formatted(...) — not string templates |
Embed """ in the content | \""" |
This does not compile — the opening delimiter is not followed by a newline:
String bad = """hello""";
This does:
String ok = """
hello
""";
Incidental indentation
The compiler looks at every non-blank content line and the line that holds the closing """. The smallest common leading whitespace is incidental. That prefix is removed from every line. Spaces that sit to the right of that prefix are content.
String html = """
<html>
<body>
<p>Hello</p>
</body>
</html>
""";
The closing """ sits under <html>. That shared indent is stripped. The extra indent on <body> and <p> is real:
<html>
<body>
<p>Hello</p>
</body>
</html>
Move the closing delimiter to column 0 and the code indent becomes part of the string:
String html = """
<html>
<body>
<p>Hello</p>
</body>
</html>
""";
Now the result starts with the spaces you used to line the block up with neighboring statements. That is almost never what you want for JSON or SQL. Park the closing """ under the content so the document starts at column 0.
Note: Blank lines do not set the indent. A completely empty line in the block becomes an empty line in the string; it does not drag the margin left.
Trailing newline
A text block includes a trailing \n when the closing """ stands on its own line. That matches how you typed the document.
String a = """
hello
""";
String b = "hello\n";
System.out.println(a.equals(b)); // true
To omit the final newline, put the closing delimiter on the last content line:
String a = """
hello""";
System.out.println(a.equals("hello")); // true
Most JSON, SQL, and HTML fixtures want the trailing newline. Drop it when the string must match a token, a header value, or a golden file that has no final LF.
Line endings inside the block are always \n (LF), even if the source file was saved with CRLF. That is a feature when tests compare strings across machines.
Escapes: \s, \, and \"""
Ordinary escapes still work (\n, \t, \\, unicode). Text blocks add two that exist because of how incidental whitespace is stripped, and one you need when the content itself contains the delimiter.
Trailing spaces: \s
After incidental indent is removed, trailing whitespace on every line is stripped. A space you cared about at the end of a line disappears. \s is a single space that survives that pass — it is translated after stripping.
String row = """
id\s\s\sname
7 \s\sAda
""";
Use \s for aligned columns and for a required trailing space. Do not sprinkle it everywhere; most documents do not need it.
Line continuation: \
A backslash immediately before the line terminator swallows that newline. The next line is joined on. Incidental indent on the continuation line is still stripped, so put the gap you want before the backslash:
String sql = """
SELECT id, email, created_at \
FROM users \
WHERE status = 'ACTIVE'
""";
That is one line: SELECT id, email, created_at FROM users WHERE status = 'ACTIVE' plus the trailing newline from the closing delimiter. Forget the space before \ and the words glue together.
Quotes inside the block
A single " or a pair "" needs no escape. Three in a row would look like the delimiter, so escape the first:
String talk = """
The literal opens with \""" and then a newline.
""";
Substitution with formatted
A text block is a String, so String.format works. Java 15 also adds the instance method that reads naturally on a literal:
String json = """
{
"name": "%s",
"age": %d
}
""".formatted(name, age);
"%s" and "%d" are the same specifiers as String.format. There is no ${name} syntax.
Note: Formatting SQL or JSON with user input is still concatenation with extra steps. Use a PreparedStatement (or your driver’s bind API) for query parameters, and a JSON library for payloads that leave the process. .formatted(...) is for fixtures, log lines, and other trusted substitutions.
String.stripIndent() runs the same incidental-indent algorithm at runtime. Text blocks already do this at compile time. Reach for stripIndent() when the multiline text arrived as an ordinary string — a resource file, a test fixture loaded from disk — and you want the same left-margin cleanup.
JSON, SQL, HTML
JSON fixture
Quotes stay quotes. Indent the object the way you want it on the wire, then align the closing """ with the opening {:
String body = """
{
"orderId": "A-1042",
"items": [
{ "sku": "ABC", "qty": 2 },
{ "sku": "XYZ", "qty": 1 }
]
}
""";
SQL for PreparedStatement
Keep placeholders as ?. The block is the query text, not the bind values:
String sql = """
SELECT u.id, u.email
FROM users u
WHERE u.status = ?
AND u.created_at >= ?
ORDER BY u.email
""";
try (PreparedStatement ps = connection.prepareStatement(sql)) {
ps.setString(1, status);
ps.setObject(2, since);
...
}
HTML snippet
Same pattern. Nested tags keep the extra indent you typed:
String fragment = """
<section class="notice">
<h2>%s</h2>
<p>%s</p>
</section>
""".formatted(title, body);
Escape HTML entities in title and body yourself (or use a template engine). The text block only gives you a readable skeleton.
When not to use text blocks
Skip them when:
- The string is one line —
"hello"is the right literal. A text block around a single word is noise plus an accidental trailing newline. - Tiny strings — a status code, a header name, a four-character token. Concatenation soup was never the problem.
- Regex — whitespace is significant, incidental stripping will move your pattern, and a block does not make a dense expression clearer. Keep the regex as a normal
"..."string (or aPatternconstant) and comment it. - Untrusted interpolation —
.formattedis not a query builder and not a JSON serializer.
If the multiline thing you actually wanted was documentation, that is a different feature. Markdown Javadoc uses adjacent /// lines; it is not a text block and it does not produce a String at runtime.
Cheat sheet
"""
content
"""
Java 15+ (JEP 378); common on Java 17 LTS
Opening """ must be followed by a newline
Closing """ column = incidental indent ruler
Own-line closer → trailing \n; same-line closer → no trailing \n
Result is still String
Quotes: " is fine; embed """ with \"""
Placeholders: .formatted(...) — no STR templates
\s = keep a space; \ at EOL = join the next line
Good: JSON, SQL, HTML, fixtures
Avoid: one-liners, regex, stuffing user input into SQL/JSON
Do:
- Put content on the line after the opening
""". - Align the closing
"""with the leftmost content you want at column 0. - Bind SQL parameters; serialize JSON with a library when the payload is real.
Don’t:
- Open a text block on the same line as the content.
- Assume the final newline is optional — check whether your golden file includes it.
- Treat
.formatted(...)as interpolation that is safe for untrusted data.
Wrap-up
Text blocks remove the ceremony around multiline strings without changing the type. They became a standard language feature in Java 15 (JEP 378) and are the default choice on Java 17+ for JSON fixtures, SQL, HTML snippets, and any other document that used to be a + chain.
Start with the opening """, a newline, and the document as you would paste it. Park the closing delimiter under the content so incidental indent disappears. Use .formatted(...) for trusted placeholders. Keep one-liners and regex as ordinary literals — and keep user data out of the string until a proper API binds or serializes it.