SCX Format JSON 是 scx-format 的 JSON 实现模块。
它负责在 JSON 文本和 scx-node 的 Node 数据模型之间互相转换。
SCX Format JSON 基于 Jackson Core 实现 JSON 读写,但对外暴露的是 Node 转换接口,而不是 Jackson 的对象模型。
当前版本为 0.3.0。
<dependency>
<groupId>dev.scx</groupId>
<artifactId>scx-format-json</artifactId>
<version>0.3.0</version>
</dependency>
scx-format-json 依赖:
scx-format
jackson-core
因此普通使用场景中,只需要引入 scx-format-json。
SCX Format JSON 中最核心的概念包括:
JsonNodeConverter JSON 和 Node 之间的转换器
JsonNodeConvertOptions JSON 转换选项
JsonSerializer Node -> JSON 的底层序列化器
JsonDeserializer JSON -> Node 的底层反序列化器
DuplicateFieldPolicy JSON 对象重复字段处理策略
LightJsonFactory 轻量 JsonFactory
它们之间的关系可以简单理解为:
JSON 文本
↓
JsonNodeConverter#formatToNode(...)
↓
JsonDeserializer
↓
Node
Node
↓
JsonNodeConverter#nodeToFormat(...)
↓
JsonSerializer
↓
JSON 文本
也就是说:
JsonNodeConverter 负责入口
JsonDeserializer 负责读取 JSON token 并生成 Node
JsonSerializer 负责把 Node 写成 JSON token
JsonNodeConvertOptions 负责配置读取和写出行为
最简单的用法是把 JSON 字符串解析成 Node。
import dev.scx.format.json.JsonNodeConvertOptions;
import dev.scx.format.json.JsonNodeConverter;
import dev.scx.node.Node;
var converter = new JsonNodeConverter();
Node node = converter.formatToNode(
"""
{
"name": "Tom",
"age": 18,
"active": true
}
""",
new JsonNodeConvertOptions()
);
再把 Node 输出回 JSON:
String json = converter.nodeToFormatString(
node,
new JsonNodeConvertOptions()
);
如果想要格式化输出:
String prettyJson = converter.nodeToFormatString(
node,
new JsonNodeConvertOptions().prettyPrint(true)
);
JsonNodeConverter 是本模块的核心入口。
它实现了 FormatNodeConverter<JsonNodeConvertOptions>。
常见方法包括:
Node formatToNode(String text, JsonNodeConvertOptions options);
Node formatToNode(byte[] bytes, Charset charset, JsonNodeConvertOptions options);
Node formatToNode(Reader reader, JsonNodeConvertOptions options)
throws IOException;
Node formatToNode(InputStream inputStream, Charset charset, JsonNodeConvertOptions options)
throws IOException;
Node formatToNode(File file, Charset charset, JsonNodeConvertOptions options)
throws IOException;
String nodeToFormatString(Node node, JsonNodeConvertOptions options);
byte[] nodeToFormatBytes(Node node, Charset charset, JsonNodeConvertOptions options);
void nodeToFormat(Node node, Writer writer, JsonNodeConvertOptions options)
throws IOException;
void nodeToFormat(Node node, OutputStream outputStream, Charset charset, JsonNodeConvertOptions options)
throws IOException;
File nodeToFormatFile(Node node, File file, Charset charset, JsonNodeConvertOptions options)
throws IOException;
示例:
var json = """
{
"user": {
"id": 12345,
"name": "小明",
"active": true,
"score": 99.99,
"tags": ["程序员", "摄影师", "旅行者"],
"updated_at": null
}
}
""";
Node node = new JsonNodeConverter().formatToNode(
json,
new JsonNodeConvertOptions()
);
转换结果大致是:
ObjectNode
user -> ObjectNode
id -> IntNode(12345)
name -> StringNode("小明")
active -> BooleanNode.TRUE
score -> DoubleNode(99.99)
tags -> ArrayNode
updated_at -> NullNode.NULL
示例:
import dev.scx.node.ArrayNode;
import dev.scx.node.ObjectNode;
import static dev.scx.node.NullNode.NULL;
var user = new ObjectNode();
user.put("id", 12345);
user.put("name", "小明");
user.put("active", true);
user.put("score", 99.99);
var tags = new ArrayNode();
tags.add("程序员");
tags.add("摄影师");
tags.add("旅行者");
user.put("tags", tags);
user.put("updated_at", NULL);
var root = new ObjectNode();
root.put("user", user);
String json = new JsonNodeConverter().nodeToFormatString(
root,
new JsonNodeConvertOptions().prettyPrint(true)
);
输出类似:
{
"user" : {
"id" : 12345,
"name" : "小明",
"active" : true,
"score" : 99.99,
"tags" : [ "程序员", "摄影师", "旅行者" ],
"updated_at" : null
}
}
具体空格和换行格式由 Jackson 的 pretty printer 决定。
JSON 序列化支持以下 Node 类型:
ObjectNode
ArrayNode
StringNode
IntNode
LongNode
FloatNode
DoubleNode
BigIntegerNode
BigDecimalNode
BooleanNode
NullNode
对应 JSON 类型可以理解为:
ObjectNode -> JSON object
ArrayNode -> JSON array
StringNode -> JSON string
IntNode -> JSON number
LongNode -> JSON number
FloatNode -> JSON number
DoubleNode -> JSON number
BigIntegerNode -> JSON number
BigDecimalNode -> JSON number
BooleanNode -> JSON boolean
NullNode -> JSON null
JSON 读取时,整数和浮点数会根据 Jackson 返回的 number type 映射为不同 Node。
整数:
INT -> IntNode
LONG -> LongNode
BIG_INTEGER -> BigIntegerNode
浮点数:
FLOAT -> FloatNode
DOUBLE -> DoubleNode
BIG_DECIMAL -> BigDecimalNode
示例:
Node node = converter.formatToNode(
"""
{
"i": 1,
"l": 1234567890123,
"d": 1.25
}
""",
new JsonNodeConvertOptions()
);
读取后会得到对应数字节点。
SCX Format JSON 不要求 JSON 顶层必须是对象或数组。
下面这些都是合法输入:
"hello"
123
true
null
例如:
Node node = converter.formatToNode(
"123",
new JsonNodeConvertOptions()
);
会得到一个数字节点。
如果输入中没有任何有效内容,会抛出 FormatToNodeException。
converter.formatToNode("", new JsonNodeConvertOptions());
如果一个合法 JSON 值后面还有多余内容,也会抛出异常。
converter.formatToNode(
"{} {}",
new JsonNodeConvertOptions()
);
这可以避免只读取前半段 JSON,而静默忽略后面的脏数据。
JSON 对象中理论上可能出现重复字段。
例如:
{
"name": "Tom",
"name": "Jerry"
}
JsonNodeConvertOptions 提供 duplicateFieldPolicy(...) 用来控制重复字段处理策略。
可选值包括:
USE_NEW 使用新值
USE_OLD 使用旧值
THROW 抛出异常
MERGE 合并为数组
默认值是:
USE_NEW
使用新值覆盖旧值。
var options = new JsonNodeConvertOptions()
.duplicateFieldPolicy(DuplicateFieldPolicy.USE_NEW);
输入:
{
"name": "Tom",
"name": "Jerry"
}
结果相当于:
{
"name": "Jerry"
}
保留旧值,忽略新值。
var options = new JsonNodeConvertOptions()
.duplicateFieldPolicy(DuplicateFieldPolicy.USE_OLD);
输入:
{
"name": "Tom",
"name": "Jerry"
}
结果相当于:
{
"name": "Tom"
}
发现重复字段时直接抛出异常。
var options = new JsonNodeConvertOptions()
.duplicateFieldPolicy(DuplicateFieldPolicy.THROW);
输入:
{
"name": "Tom",
"name": "Jerry"
}
会抛出 FormatToNodeException。
把重复字段合并成数组。
var options = new JsonNodeConvertOptions()
.duplicateFieldPolicy(DuplicateFieldPolicy.MERGE);
输入:
{
"name": "Tom",
"name": "Jerry"
}
结果相当于:
{
"name": ["Tom", "Jerry"]
}
如果旧值已经是数组,则会继续往这个数组中追加新值。
JsonNodeConvertOptions 支持一些 Jackson JSON read features。
例如允许 Java 风格注释:
var options = new JsonNodeConvertOptions()
.allowJavaComments(true);
允许 YAML 风格注释:
var options = new JsonNodeConvertOptions()
.allowYamlComments(true);
允许单引号:
var options = new JsonNodeConvertOptions()
.allowSingleQuotes(true);
允许未加引号的属性名:
var options = new JsonNodeConvertOptions()
.allowUnquotedPropertyNames(true);
允许尾随逗号:
var options = new JsonNodeConvertOptions()
.allowTrailingComma(true);
允许缺失值:
var options = new JsonNodeConvertOptions()
.allowMissingValues(true);
这些选项适合处理非标准 JSON 或兼容历史数据。
如果你需要严格 JSON,应保持默认配置。
可配置的数字读取相关选项包括:
new JsonNodeConvertOptions()
.allowLeadingDecimalPointForNumbers(true)
.allowLeadingPlusSignForNumbers(true)
.allowLeadingZerosForNumbers(true)
.allowNonNumericNumbers(true)
.allowTrailingDecimalPointForNumbers(true);
它们分别用于控制:
.5
+1
001
NaN / Infinity
1.
这类非标准 JSON 数字形式。
可配置的写出选项包括:
new JsonNodeConvertOptions()
.quotePropertyNames(true)
.writeNanAsStrings(true)
.escapeNonAscii(false)
.writeNumbersAsStrings(false);
含义可以理解为:
quotePropertyNames 是否给属性名加引号
writeNanAsStrings 是否把 NaN 写为字符串
escapeNonAscii 是否转义非 ASCII 字符
writeNumbersAsStrings 是否把数字写为字符串
示例:转义非 ASCII 字符。
var json = converter.nodeToFormatString(
node,
new JsonNodeConvertOptions().escapeNonAscii(true)
);
prettyPrint(true) 用于格式化输出。
var options = new JsonNodeConvertOptions()
.prettyPrint(true);
String json = converter.nodeToFormatString(node, options);
未开启 pretty print 时,输出更紧凑。
var options = new JsonNodeConvertOptions()
.prettyPrint(false);
默认值是:
false
JsonNodeConvertOptions 支持设置读写约束。
var options = new JsonNodeConvertOptions()
.maxNestingDepth(200)
.maxDocumentLength(10_000_000)
.maxTokenCount(1_000_000)
.maxNumberLength(1_000)
.maxStringLength(1_000_000)
.maxNameLength(10_000);
这些选项用于限制输入规模,避免异常大输入造成资源消耗过高。
常见用途:
限制最大嵌套深度
限制最大文档长度
限制最大 token 数量
限制数字长度
限制字符串长度
限制字段名长度
maxNestingDepth(...) 会影响 JSON 解析时允许的最大嵌套层级。
示例:
var json = "[".repeat(100) + "]".repeat(100);
converter.formatToNode(
json,
new JsonNodeConvertOptions().maxNestingDepth(80)
);
因为输入嵌套深度超过 80,所以会抛出 FormatToNodeException。
这个限制也间接保护递归序列化时的栈深度。
如果需要自定义字符转义,可以设置 characterEscapes(...)。
var options = new JsonNodeConvertOptions()
.characterEscapes(myCharacterEscapes);
这个选项直接传给底层 Jackson generator。
可以设置根值分隔符。
var options = new JsonNodeConvertOptions()
.rootValueSeparator(mySeparator);
通常只有在连续写多个 root value 的特殊场景下才需要关注。
可以设置最高非转义字符。
var options = new JsonNodeConvertOptions()
.highestNonEscapedChar(127);
这类配置适合对输出字符集或转义策略有特殊要求的场景。
默认 JSON 字符串和字段名使用双引号。
"
可以通过 quoteChar(...) 修改。
var options = new JsonNodeConvertOptions()
.quoteChar('\'');
需要注意,修改 quote char 可能会生成非标准 JSON。
可以从字符流读取:
try (var reader = new FileReader(file, StandardCharsets.UTF_8)) {
Node node = converter.formatToNode(
reader,
new JsonNodeConvertOptions()
);
}
可以从字节流读取:
try (var inputStream = new FileInputStream(file)) {
Node node = converter.formatToNode(
inputStream,
StandardCharsets.UTF_8,
new JsonNodeConvertOptions()
);
}
可以直接从文件读取:
Node node = converter.formatToNode(
file,
StandardCharsets.UTF_8,
new JsonNodeConvertOptions()
);
输出到字符流:
try (var writer = new FileWriter(file, StandardCharsets.UTF_8)) {
converter.nodeToFormat(
node,
writer,
new JsonNodeConvertOptions().prettyPrint(true)
);
}
输出到字节流:
try (var outputStream = new FileOutputStream(file)) {
converter.nodeToFormat(
node,
outputStream,
StandardCharsets.UTF_8,
new JsonNodeConvertOptions().prettyPrint(true)
);
}
输出到文件:
converter.nodeToFormatFile(
node,
file,
StandardCharsets.UTF_8,
new JsonNodeConvertOptions().prettyPrint(true)
);
JSON 解析失败时,会抛出 FormatToNodeException。
try {
Node node = converter.formatToNode(text, options);
} catch (FormatToNodeException e) {
// JSON -> Node 失败
}
Node 输出 JSON 失败时,会抛出 NodeToFormatException。
try {
String json = converter.nodeToFormatString(node, options);
} catch (NodeToFormatException e) {
// Node -> JSON 失败
}
如果使用 Reader、InputStream、Writer、OutputStream 或 File,还可能出现 IOException。
try {
Node node = converter.formatToNode(file, StandardCharsets.UTF_8, options);
} catch (FormatToNodeException e) {
// JSON 内容错误
} catch (IOException e) {
// 文件读取错误
}
import dev.scx.format.json.JsonNodeConvertOptions;
import dev.scx.format.json.JsonNodeConverter;
import dev.scx.node.ArrayNode;
import dev.scx.node.ObjectNode;
import static dev.scx.node.NullNode.NULL;
public class JsonFormatDemo {
public static void main(String[] args) {
var converter = new JsonNodeConverter();
var root = new ObjectNode();
var user = new ObjectNode();
user.put("id", 12345);
user.put("name", "小明");
user.put("active", true);
user.put("score", 99.99);
user.put("updated_at", NULL);
var tags = new ArrayNode();
tags.add("程序员");
tags.add("摄影师");
tags.add("旅行者");
user.put("tags", tags);
root.put("user", user);
var options = new JsonNodeConvertOptions()
.prettyPrint(true);
String json = converter.nodeToFormatString(root, options);
System.out.println(json);
var node2 = converter.formatToNode(json, new JsonNodeConvertOptions());
System.out.println(root.equals(node2));
}
}
使用者通常只需要直接使用:
new JsonNodeConverter()
不需要直接操作 JsonSerializer 或 JsonDeserializer。
本模块使用 Jackson Core 的 streaming API。
它不依赖 Jackson Databind,也不使用 Jackson 的 POJO 绑定模型。
转换边界是:
JSON <-> Node
不是:
JSON <-> Java Bean
JSON 读取时,对象和数组容器使用栈结构处理。
这样可以避免深层 JSON 读取时直接依赖 Java 方法递归。
但序列化阶段仍然是递归下降方式。
因此仍然需要通过 maxNestingDepth(...) 限制极端深度。
JsonSerializer 会根据具体 Node 类型调用对应的 JSON 写出方法。
例如:
ObjectNode writeStartObject / writeName / writeEndObject
ArrayNode writeStartArray / writeEndArray
StringNode writeString
IntNode writeNumber
BooleanNode writeBoolean
NullNode writeNull
DuplicateFieldPolicy 不是 scx-format 的核心语义。
它只属于 JSON 读取过程。
因为 JSON 对象中重复字段如何处理,并不是所有格式都共有的问题。
JsonNodeConvertOptions 的 setter 方法会返回 this。
因此可以链式配置:
var options = new JsonNodeConvertOptions()
.prettyPrint(true)
.allowTrailingComma(true)
.duplicateFieldPolicy(DuplicateFieldPolicy.MERGE);
JsonNodeConverter 构造时会创建并复用部分较重对象。
因此推荐复用同一个 JsonNodeConverter 实例,而不是每次转换都创建新实例。
private static final JsonNodeConverter JSON = new JsonNodeConverter();
不是。
它只负责:
JSON <-> Node
如果需要:
Node <-> Java Object
应使用其它对象绑定模块。
使用。
底层基于 Jackson Core。
不使用。
它不通过 Jackson Databind 绑定 Java Bean。
不会。
默认 prettyPrint 是 false。
需要格式化时:
new JsonNodeConvertOptions().prettyPrint(true)
支持。
顶层不要求必须是对象或数组。
默认使用新值。
DuplicateFieldPolicy.USE_NEW
new JsonNodeConvertOptions()
.duplicateFieldPolicy(DuplicateFieldPolicy.THROW)
new JsonNodeConvertOptions()
.duplicateFieldPolicy(DuplicateFieldPolicy.MERGE)
默认按 Jackson 默认值。
如果需要允许 Java 注释:
new JsonNodeConvertOptions().allowJavaComments(true)
如果需要允许 YAML 注释:
new JsonNodeConvertOptions().allowYamlComments(true)
可以开启:
new JsonNodeConvertOptions().allowSingleQuotes(true)
可以开启:
new JsonNodeConvertOptions().allowTrailingComma(true)
new JsonNodeConvertOptions().maxNestingDepth(200)
formatToNode 会自动关闭我传入的 Reader 吗?不会。
外部传入资源由调用方关闭。
nodeToFormat 会自动关闭我传入的 Writer 吗?不会。
外部传入资源由调用方关闭。
抛 FormatToNodeException。
底层 Jackson 异常会作为 cause 保存。
抛 NodeToFormatException。
可以。
byte[] bytes = converter.nodeToFormatBytes(
node,
StandardCharsets.UTF_8,
options
);
可以。
converter.nodeToFormatFile(
node,
file,
StandardCharsets.UTF_8,
options
);
对于 JSON 能表达的标准数据结构,通常是稳定的。
例如:
JSON -> Node -> JSON -> Node
前后两个 Node 相等。
但如果你开启某些非标准选项,或者输入中包含重复字段,最终结果会受到选项影响。