JEP 430:字符串模板(预览)
概括
_使用字符串模板_增强 Java 编程语言。字符串模板通过将文字文本与嵌入式表达式和_模板处理器_耦合来生成专门的结果,从而补充了 Java 现有的字符串文字和文本块。这是预览语言功能和 API。
目标
-
通过轻松表达包含运行时计算值的字符串,简化 Java 程序的编写。
-
增强混合文本和表达式的表达式的可读性,无论文本适合单个源行(如字符串文字)还是跨越多个源行(如文本块)。
-
通过支持模板及其嵌入表达式的值的验证和转换,提高 Java 程序的安全性,这些程序从用户提供的值组成字符串并将其传递到其他系统(例如,构建数据库查询)。
-
通过允许 Java 库定义字符串模板中使用的格式化语法来保持灵活性。
-
简化接受非 Java 语言(例如 SQL、XML 和 JSON)编写的字符串的 API 的使用。
-
允许创建根据文字文本和嵌入表达式计算的非字符串值,而无需通过中间字符串表示形式进行传输。
非目标
-
为 Java 的字符串连接运算符 ( ) 引入语法糖并不是我们的目标
+
,因为这会绕过验证的目标。 -
StringBuilder
弃用或删除和类并不是我们的目标StringBuffer
,它们传统上用于复杂或编程的字符串组合。
动机
开发人员通常会根据文字文本和表达式的组合来组成字符串。 Java 提供了多种字符串组合机制,但遗憾的是它们都有缺点。
-
+
使用运算符连接字符串会产生难以阅读的代码:String s = x + " plus " + y + " equals " + (x + y);
-
StringBuilder
很冗长:String s = new StringBuilder()
.append(x)
.append(" plus ")
.append(y)
.append(" equals ")
.append(x + y)
.toString(); -
String::format
并将String::formatted
格式字符串与参数分开,从而导致数量和类型不匹配:String s = String.format("%2$d plus %1$d equals %3$d", x, y, x + y);
String t = "%2$d plus %1$d equals %3$d".formatted(x, y, x + y); -
java.text.MessageFormat
需要太多仪式并在格式字符串中使用不熟悉的语法:MessageFormat mf = new MessageFormat("{0} plus {1} equals {2}");
String s = mf.format(x, y, x + y);
字符串插值
许多编程语言提供_字符串插值_作为字符串连接的替代方法。通常,它采用包含嵌入表达式和文字文本的字符串 文字的形式。_原位_嵌入表达式意味着读者可以轻松辨别预期结果。在运行时,嵌入的表达式将被替换为它们的(字符串化的)值——这些值被认为是被_插入_到字符串中的。以下是其他语言中的插值的一些示例:
C# $"{x} plus {y} equals {x + y}"
Visual Basic $"{x} plus {y} equals {x + y}"
Python f"{x} plus {y} equals {x + y}"
Scala s"$x plus $y equals ${x + y}"
Groovy "$x plus $y equals ${x + y}"
Kotlin "$x plus $y equals ${x + y}"
JavaScript `${x} plus ${y} equals ${x + y}`
Ruby "#{x} plus #{y} equals #{x + y}"
Swift "\(x) plus \(y) equals \(x + y)"
其中一些语言支持对所有字符串文字进行插值,而其他语言则需要在需要时启用插值,例如通过在文字的开始分隔符前添加$
或前缀f
。嵌入表达式的语法也有所不同,但通常涉及诸如$
或 之类的字符{ }
,这意味着这些字符不能按字面意思显示,除非进行转义。
编写代码时插值不仅比串联更方便,而且在阅读代码时也更清晰。对于较大的琴弦,其清晰度尤其引人注目。例如,在 JavaScript 中:
const title = "My Web Page";
const text = "Hello, world";
var html = `<html>
<head>
<title>${title}</title>
</head>
<body>
<p>${text}</p>
</body>
</html>`;
字符串插值是危险的
不幸的是,插值的便利性有一个缺点:很容易构造可以被其他系统解释的字符串,但在这些系统中却是危险的错误。
包含 SQL 语句、HTML/XML 文档、JSON 片段、shell 脚本和自然语言文本的字符串都需要根据特定于域的规则进行验证和清理。由于 Java 编程语言不可能强制执行所有此类规则,因此开发人员需要使用插值来进行验证和清理。通常,这意味着记住将嵌入式表达式包装在对escape
或validate
方法的调用中,并依靠 IDE 或静态分析工具来帮助验证文字文本。
插值对于 SQL 语句尤其危险,因为它可能导致注入攻击。例如,考虑这个带有嵌入表达式的假设 Java 代码${name}
:
String query = "SELECT * FROM Person p WHERE p.last_name = '${name}'";
ResultSet rs = connection.createStatement().executeQuery(query);
如果name
有麻烦的值
Smith' OR p.last_name <> 'Smith
那么查询字符串将是
SELECT * FROM Person p WHERE p.last_name = 'Smith' OR p.last_name <> 'Smith'
并且代码将选择所有行,可能会泄露 机密信息。使用简单的插值组合查询字符串与使用传统连接组合查询字符串一样不安全:
String query = "SELECT * FROM Person p WHERE p.last_name = '" + name + "'";
我们可以做得更好吗?
对于 Java,我们希望有一个字符串组合功能,既能实现插值的清晰度,又能获得开箱即用的更安全的结果,也许会牺牲少量的便利性来获得大量的安全性。
例如,在编写 SQL 语句时,必须对嵌入表达式的值中的任何引号进行转义,并且整个字符串必须具有平衡的引号。考虑到上面显示的麻烦值name
,应该组成的查询是安全的:
SELECT * FROM Person p WHERE p.last_name = '\'Smith\' OR p.last_name <> \'Smith\''
几乎每次字符串插值的使用都涉及构建字符串以适应某种模板:SQL 语句通常遵循模板SELECT ... FROM ... WHERE ...
,HTML 文档遵循<html> ... </html>
,甚至自然语言的消息也遵循散布动态值的模板(例如,用户名) ) 在文字文本中。每种模板都有验证和转换规则,例如 SQL 语句的“转义所有引号”、HTML 文档的“仅允许合法字符实体”以及自然语言消息的“本地化到操作系统中配置的语言”。
理想情况下,字符串的模板可以直接在代码中表达,就像注释字符串一样,并且 Java 运行时会自动将特定于模板的规则应用于字符串。结果将是带有转义引号的 SQL 语句、没有非法实体的 HTML 文档以及无样板的消息本地化。从模板组成字符串将使开发人员不必费力地转义每个嵌入的表达式、调用validate()
整个字符串或用于java.util.ResourceBundle
查找本地化字符串。
再举个例子,我们可以构造一个表示 JSON 文档的字符串,然后将其提供给 JSON 解析器以获得强类型JSONObject
:
String name = "Joan Smith";
String phone = "555-123-4567";
String address = "1 Maple Drive, Anytown";
String json = """
{
"name": "%s",
"phone": "%s",
"address": "%s"
}
""".formatted(name, phone, address);
JSONObject doc = JSON.parse(json);
... doc.entrySet().stream().map(...) ...
理想情况下,字符串的 JSON 结构可以直接在代码中表达,Java 运行时会JSONObject
自动将字符串转换为 .不需要手动绕过解析器。
总之,如果我们有一流的、基于模板的字符串组合机制,我们就可以提高几乎每个 Java 程序的可读性和可靠性。正如其他编程语言中所见,这样的功能将提供插值的好处,但不太容易引入安全漏洞。它还将减少与将复杂输入作为字符串的库合作的仪式。
描述
_模板表达式_是Java编程语言中的一种新的表达式。模板表达式可以执行字符串插值,但也可以通过编程方式帮助开发人员安全有效地编写字符串。此外,模板表达式不仅限于组成字符串——它们可以根据特定领域的规则将结构化文本转换为任何类型的对象。
从语法上讲,模板表达式类似于带有前缀的字符串文字。该代码的第二行有一个模板表达式:
String name = "Joan";
String info = STR."My name is \{name}";
assert info.equals("My name is Joan"); // true
模板表达式STR."My name is \{name}"
包括:
- 模板_处理器_(
STR
); - 点字符 (U+002E),如其他类型的表达式中所示;和
- 包含_嵌入表达式_( )的模板( ) 。
"My name is \{name}"``\{name}
当在运行时计算模板表达式时,其模板处理器将模板中的文字文本与嵌入表达式的值组合起来,以生成结果。模板处理器的结果,以及评估模板表达式的结果,通常是 a String
— 尽管并非总是如此。
模板STR
处理器
STR
是Java平台中定义的模板处理器。它通过将模板中的每个嵌入表达式替换为该表达式的(字符串化)值来执行字符串插值。使用的模板表达式的求值结果STR
是String
;例如,"My name is Joan"
。
在日常对话中,开发人员在指代整个模板表达式(其中包括模板处理器)或仅指模板表达式的模板部分(即模板处理器的参数)时,可能会使用术语“模板”。只要注意不要混淆这些概念,这种非正式用法就是合理的。
STR
是一个public
static
final
自动导入到每个 Java 源文件中的字段。
以下是使用STR
模板处理器的模板表达式的更多示例。左边距中的符号|
表示该行显示上一条语句的值,类似于jshell。
// Embedded expressions can be strings
String firstName = "Bill";
String lastName = "Duck";
String fullName = STR."\{firstName} \{lastName}";
| "Bill Duck"
String sortName = STR."\{lastName}, \{firstName}";
| "Duck, Bill"
// Embedded expressions can perform arithmetic
int x = 10, y = 20;
String s = STR."\{x} + \{y} = \{x + y}";
| "10 + 20 = 30"
// Embedded expressions can invoke methods and access fields
String s = STR."You have a \{getOfferType()} waiting for you!";
| "You have a gift waiting for you!"
String t = STR."Access at \{req.date} \{req.time} from \{req.ipAddress}";
| "Access at 2022-03-25 15:34 from 8.8.8.8"
为了帮助重构,可以在嵌入表达式中使用双引号字符,而无需将其转义为\"
。这意味着嵌入表达式在模板表达式中的显示方式与在模板表达式外部的显示方式完全相同,从而简化了从串联 ( +
) 到模板表达式的切换。例如:
String filePath = "tmp.dat";
File file = new File(filePath);
String old = "The file " + filePath + " " + (file.exists() ? "does" : "does not") + " exist";
String msg = STR."The file \{filePath} \{file.exists() ? "does" : "does not"} exist";
| "The file tmp.dat does exist" or "The file tmp.dat does not exist"
为了提高可读性,嵌入表达式可以分布在源文件中的多行中,而无需在结果中引入换行符。将嵌入表达式的值插值到结果中\
嵌入表达式的位置;然后,该模板将被视为与 继续在同一行上\
。例如:
String time = STR."The time is \{
// The java.time.format package is very useful
DateTimeFormatter
.ofPattern("HH:mm:ss")
.format(LocalTime.now())
} right now";
| "The time is 12:34:56 right now"
字符串模板表达式中嵌入表达式的数量没有限制。嵌入表达式从左到右计算,就像方法调用表达式中的参数一样。例如:
// Embedded expressions can be postfix increment expressions
int index = 0;
String data = STR."\{index++}, \{index++}, \{index++}, \{index++}";
| "0, 1, 2, 3"
任何 Java 表达式都可以用作嵌入表达式 — 甚至是模板表达式。例如:
// Embedded expression is a (nested) template expression
String[] fruit = { "apples", "oranges", "peaches" };
String s = STR."\{fruit[0]}, \{STR."\{fruit[1]}, \{fruit[2]}"}";
| "apples, oranges, peaches"
这里模板表达式STR."\{fruit[1]}, \{fruit[2]}"
嵌入到另一个模板表达式的模板中。由于"
、\
、 和{ }
字符较多,此代码难以阅读,因此最好将其格式化为:
String s = STR."\{fruit[0]}, \{
STR."\{fruit[1]}, \{fruit[2]}"
}";
或者,由于嵌入的表达式没有副作用,因此可以将其重构为单独的模板表达式:
String tmp = STR."\{fruit[1]}, \{fruit[2]}";
String s = STR."\{fruit[0]}, \{tmp}";
多行模板表达式
模板表达式的模板可以跨越多行源代码,使用类似于文本块的语法。 (我们在上面看到了一个跨越多行的嵌入表达式,但包含嵌入表达式的模板在逻辑上是一行。)
以下是表示 HTML 文本、JSON 文本和区域表的模板表达式示例,全部分布在多行中:
String title = "My Web Page";
String text = "Hello, world";
String html = STR."""
<html>
<head>
<title>\{title}</title>
</head>
<body>
<p>\{text}</p>
</body>
</html>
""";
| """
| <html>
| <head>
| <title>My Web Page</title>
| </head>
| <body>
| <p>Hello, world</p>
| </body>
| </html>
| """
String name = "Joan Smith";
String phone = "555-123-4567";
String address = "1 Maple Drive, Anytown";
String json = STR."""
{
"name": "\{name}",
"phone": "\{phone}",
"address": "\{address}"
}
""";
| """
| {
| "name": "Joan Smith",
| "phone": "555-123-4567",
| "address": "1 Maple Drive, Anytown"
| }
| """
record Rectangle(String name, double width, double height) {
double area() {
return width * height;
}
}
Rectangle[] zone = new Rectangle[] {
new Rectangle("Alfa", 17.8, 31.4),
new Rectangle("Bravo", 9.6, 12.4),
new Rectangle("Charlie", 7.1, 11.23),
};
String table = STR."""
Description Width Height Area
\{zone[0].name} \{zone[0].width} \{zone[0].height} \{zone[0].area()}
\{zone[1].name} \{zone[1].width} \{zone[1].height} \{zone[1].area()}
\{zone[2].name} \{zone[2].width} \{zone[2].height} \{zone[2].area()}
Total \{zone[0].area() + zone[1].area() + zone[2].area()}
""";
| """
| Description Width Height Area
| Alfa 17.8 31.4 558.92
| Bravo 9.6 12.4 119.03999999999999
| Charlie 7.1 11.23 79.733
| Total 757.693
| """
模板FMT
处理器
FMT
是 Java 平台中定义的另一个模板处理器。就像它执行插值FMT
一样STR
,但它也解释出现在嵌入表达式左侧的格式说明符。格式说明符与 中定义的相同java.util.Formatter
。这是区域表示例,由模板中的格式说明符整理:
record Rectangle(String name, double width, double height) {
double area() {
return width * height;
}
}
Rectangle[] zone = new Rectangle[] {
new Rectangle("Alfa", 17.8, 31.4),
new Rectangle("Bravo", 9.6, 12.4),
new Rectangle("Charlie", 7.1, 11.23),
};
String table = FMT."""
Description Width Height Area
%-12s\{zone[0].name} %7.2f\{zone[0].width} %7.2f\{zone[0].height} %7.2f\{zone[0].area()}
%-12s\{zone[1].name} %7.2f\{zone[1].width} %7.2f\{zone[1].height} %7.2f\{zone[1].area()}
%-12s\{zone[2].name} %7.2f\{zone[2].width} %7.2f\{zone[2].height} %7.2f\{zone[2].area()}
\{" ".repeat(28)} Total %7.2f\{zone[0].area() + zone[1].area() + zone[2].area()}
""";
| """
| Description Width Height Area
| Alfa 17.80 31.40 558.92
| Bravo 9.60 12.40 119.04
| Charlie 7.10 11.23 79.73
| Total 757.69
| """
确保安全
模板表达式STR."..."
是调用模板处理器process
方法的快捷方式STR
。也就是现在大家熟悉的例子:
String name = "Joan";
String info = STR."My name is \{name}";
相当于:
String name = "Joan";
StringTemplate st = RAW."My name is \{name}";
String info = STR.process(st);
其中RAW
是生成未处理对象的标准模板处理器StringTemplate
。
模板表达式的设计故意使得不可能直接从带有嵌入表达式的字符串文字或文本块转到String
带有内插表达式值的a。这可以防止危险的错误字符串在程序中传播。字符串文字由模板处理器处理,该处理器明确负责安全地插值和验证结果String
或其他。因此,如果我们忘记使用模板处理器,例如STR
、RAW
、 ,FMT
则会报告编译时错误:
String name = "Joan";
String info = "My name is \{name}";
| error: processor missing from template expression
语法和语义
模板表达式中的四种模板由其语法显示,语法从 开始TemplateExpression
:
TemplateExpression:
TemplateProcessor . TemplateArgument
TemplateProcessor:
Expression
TemplateArgument:
Template
StringLiteral
TextBlock
Template:
StringTemplate
TextBlockTemplate
StringTemplate:
Resembles a StringLiteral but has one or more embedded expressions,
and can be spread over multiple lines of source code
TextBlockTemplate:
Resembles a TextBlock but has one or more embedded expressions
Java 编译器扫描该术语并根据嵌入表达式的存在来"..."
确定是将其解析为 aStringLiteral
还是 a 。StringTemplate
编译器类似地扫描该术语"""..."""
并确定是将其解析为 aTextBlock
还是 a TextBlockTemplate
。我们统一将...
这些术语的一部分称为字符串文字、字符串模板、文本块或文本块模板的_内容。_
我们强烈鼓励 IDE 在视觉上区分字符串模板和字符串文字,以及文本块模板和文本块。在字符串模板或文本块模板的内容中,IDE 应在视觉上区分嵌入表达式和文字文本。
Java 编程语言将字符串文字与字符串模板、文本块与文本块模板区分开来,主要是因为字符串模板或文本块模板的类型不熟悉java.lang.String
。字符串模板或文本块模板的类型是java.lang.StringTemplate
,它是一个接口,并且String
不实现StringTemplate
。因此,当模板表达式的模板是字符串文字或文本块时,Java 编译器会自动将String
模板表示的内容转换为StringTemplate
不嵌入表达式。
在运行时,模板表达式的计算如下:
-
计算点左侧的表达式以获得嵌套接口的实例
StringTemplate.Processor
,即模板处理器。 -
计算点右侧的表达式以获得 的实例
StringTemplate
。 -
该
StringTemplate
实例被传递给process
该实例的方法StringTemplate.Processor
,该方法组成一个结果。
模板表达式的类型是实例process
方法的返回类型StringTemplate.Processor
。
模板处理器在运行时执行,而不是在编译时执行,因此它们无法对模板执行编译时处理。他们也无法获得源代码中模板中出现的确切字符;仅嵌入表达式的值可用,而不是嵌入表达式本身。
模板表达式内的字符串文字
使用字符串文字或文本块作为模板参数的能力提高了模板表达式的灵活性。开发人员可以编写最初在字符串文字中包含占位符文本的模板表达式,例如
String s = STR."Welcome to your account";
| "Welcome to your account"
并逐渐将表达式嵌入到文本中以创建字符串模板,而无需更改任何分隔符或插入任何特殊前缀:
String s = STR."Welcome, \{user.firstName()}, to your account \{user.accountNumber()}";
| "Welcome, Lisa, to your account 12345"
用户定义的模板处理器
之前我们看到了模板处理器STR
和FMT
,这使得模板处理器看起来像是通过字段访问的对象。这是有用的简写,但更准确地说,模板处理器是一个对象, 它是功能接口的实例StringTemplate.Processor
。特别是,该对象的类实现该接口的单个抽象方法,process
该方法接受StringTemplate
并返回一个对象。静态字段仅STR
存储此类的实例。 (存储实例的实际类STR
有一个process
执行无状态插值的方法,单例实例适合该插值,因此字段名称大写。)
开发人员可以轻松创建模板处理器以在模板表达式中使用。然而,在讨论如何创建模板处理器之前,我们必须讨论该类StringTemplate
。
的实例StringTemplate
表示在模板表达式中显示为模板的字符串模板或文本块模板。考虑这段代码:
int x = 10, y = 20;
StringTemplate st = RAW."\{x} plus \{y} equals \{x + y}";
String s = st.toString();
| StringTemplate{ fragments = [ "", " plus ", " equals ", "" ], values = [10, 20, 30] }
结果也许是令人惊讶的。10
、20
、 和30
插入到文本" plus "
和中的位置在哪里" equals "
?回想一下,模板表达式的目标之一是提供安全的字符串组合。StringTemplate::toString
简单地将, "10"
, " plus "
, "20"
," equals "
和连接"30"
成 aString
就可以绕过这个目标。相反,该toString
方法呈现 a 的两个有用部分StringTemplate
:
- 文本_片段_,
"", " plus ", " equals ", ""
, 和 - 价值,,,。
10``20``30
该类StringTemplate
直接公开这些部分:
-
StringTemplate::fragments
返回字符串模板或文本块模板中嵌入表达式之前和之后的文本片段的列表:int x = 10, y = 20;
StringTemplate st = RAW."\{x} plus \{y} equals \{x + y}";
List<String> fragments = st.fragments();
String result = String.join("\\{}", fragments);
| "\{} plus \{} equals \{}" -
StringTemplate::values
返回通过按嵌入表达式在源代码中出现的顺序计算嵌入表达式而生成的值的列表。在当前示例中,这相当于List.of(x, y, x + y)
.int x = 10, y = 20;
StringTemplate st = RAW."\{x} plus \{y} equals \{x + y}";
List<Object> values = st.values();
| [10, 20, 30]
fragments()
a 的 a在StringTemplate
模板表达式的所有计算中都是恒定的,而values()
对于每次计算都是新鲜计算的。例如:
int y = 20;
for (int x = 0; x < 3; x++) {
StringTemplate st = RAW."\{x} plus \{y} equals \{x + y}";
System.out.println(st);
}
| ["Adding ", " and ", " yields ", ""](0, 20, 20)
| ["Adding ", " and ", " yields ", ""](1, 20, 21)
| ["Adding ", " and ", " yields ", ""](2, 20, 22)
使用fragments()
and values()
,我们可以通过将 lambda 表达式传递给静态工厂方法来轻松创建插值模板处理器StringTemplate.Processor::of
:
var INTER = StringTemplate.Processor.of((StringTemplate st) -> {
String placeHolder = "•";
String stencil = String.join(placeHolder, st.fragments());
for (Object value : st.values()) {
String v = String.valueOf(value);
stencil = stencil.replaceFirst(placeHolder, v);
}
return stencil;
});
int x = 10, y = 20;
String s = INTER."\{x} plus \{y} equals \{x + y}";
| 10 plus 20 equals 30
我们可以利用每个模板代表片段和值的交替序列这一事实,通过从片段和值构建其结果,使该插值模板处理器更加高效:
var INTER = StringTemplate.Processor.of((StringTemplate st) -> {
StringBuilder sb = new StringBuilder();
Iterator<String> fragIter = st.fragments().iterator();
for (Object value : st.values()) {
sb.append(fragIter.next());
sb.append(value);
}
sb.append(fragIter.next());
return sb.toString();
});
int x = 10, y = 20;
String s = INTER."\{x} plus \{y} equals \{x + y}";
| 10 and 20 equals 30
实用程序方法StringTemplate::interpolate
执行相同的操作,连续连接片段和值:
var INTER = StringTemplate.Processor.of(StringTemplate::interpolate);
鉴于嵌入表达式的值通常是不可预测的,模板处理器通常不值得String
对其生成的值进行实习。例如,STR
不实习其结果。然而,如果需要的话,创建一个实习和插值模板处理器是很简单的:
var INTERN = StringTemplate.Processor.of(st -> st.interpolate().intern());
模板处理器 API
到目前为止,所有示例都使用工厂方法创建了模板处理器StringTemplate.Processor::of
。这些示例处理器返回 的实例String
并且不会引发任何异常,因此使用它们的模板表达式将始终成功求值。
相反,StringTemplate.Processor
直接实现接口的模板处理器可以是完全通用的。它可以返回任何类型的对象,而不仅仅是String
.如果处理失败(因为模板无效或由于某些其他原因(例如 I/O 错误)),它还可能引发已检查的异常。如果处理器抛出已检查的异常,则在模板表达式中使用它的开发人员必须使用try-catch
语句处理处理失败,否则将异常传播给调用者。
接口的声明StringTemplate.Processor
是:
package java.lang;
public interface StringTemplate {
...
@FunctionalInterface
public interface Processor<R, E extends Throwable> {
R process(StringTemplate st) throws E;
}
...
}
前面显示的插入字符串的代码:
var INTER = StringTemplate.Processor.of(StringTemplate::interpolate);
...
String s = INTER."\{x} plus \{y} equals \{x + y}";
相当于:
StringTemplate.Processor<String, RuntimeException> INTER =
StringTemplate.Processor.of(StringTemplate::interpolate);
...
String s = INTER."\{x} plus \{y} equals \{x + y}";
模板处理器的返回类型INTER
由第一个类型参数 指定String
。处理器抛出的异常由第二个类型参数指定,在本例中是RuntimeException
因为该处理器不会抛出任何已检查的异常。
这是一个模板处理器,它返回的不是字符串,而是 的实例JSONObject
:
var JSON = StringTemplate.Processor.of(
(StringTemplate st) -> new JSONObject(st.interpolate())
);
String name = "Joan Smith";
String phone = "555-123-4567";
String address = "1 Maple Drive, Anytown";
JSONObject doc = JSON."""
{
"name": "\{name}",
"phone": "\{phone}",
"address": "\{address}"
};
""";