SCX

SCX Format JSON

SCX Format JSON 是 scx-format 的 JSON 实现模块。

它负责在 JSON 文本和 scx-node 的 Node 数据模型之间互相转换。

SCX Format JSON 基于 Jackson Core 实现 JSON 读写,但对外暴露的是 Node 转换接口,而不是 Jackson 的对象模型。

GitHub

安装

Maven

<dependency>
    <groupId>dev.scx</groupId>
    <artifactId>scx-format-json</artifactId>
    <version>0.10.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

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;

JSON 转 Node

示例:

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

Node 转 JSON

示例:

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 决定。

支持的 Node 类型

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 数字映射

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()
);

读取后会得到对应数字节点。

标量 JSON

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,而静默忽略后面的脏数据。

DuplicateFieldPolicy

JSON 对象中理论上可能出现重复字段。

例如:

{
  "name": "Tom",
  "name": "Jerry"
}

JsonNodeConvertOptions 提供 duplicateFieldPolicy(...) 用来控制重复字段处理策略。

可选值包括:

USE_NEW    使用新值
USE_OLD    使用旧值
THROW      抛出异常
MERGE      合并为数组

默认值是:

USE_NEW

USE_NEW

使用新值覆盖旧值。

var options = new JsonNodeConvertOptions()
    .duplicateFieldPolicy(DuplicateFieldPolicy.USE_NEW);

输入:

{
  "name": "Tom",
  "name": "Jerry"
}

结果相当于:

{
  "name": "Jerry"
}

USE_OLD

保留旧值,忽略新值。

var options = new JsonNodeConvertOptions()
    .duplicateFieldPolicy(DuplicateFieldPolicy.USE_OLD);

输入:

{
  "name": "Tom",
  "name": "Jerry"
}

结果相当于:

{
  "name": "Tom"
}

THROW

发现重复字段时直接抛出异常。

var options = new JsonNodeConvertOptions()
    .duplicateFieldPolicy(DuplicateFieldPolicy.THROW);

输入:

{
  "name": "Tom",
  "name": "Jerry"
}

会抛出 FormatToNodeException。

MERGE

把重复字段合并成数组。

var options = new JsonNodeConvertOptions()
    .duplicateFieldPolicy(DuplicateFieldPolicy.MERGE);

输入:

{
  "name": "Tom",
  "name": "Jerry"
}

结果相当于:

{
  "name": ["Tom", "Jerry"]
}

如果旧值已经是数组,则会继续往这个数组中追加新值。

宽松 JSON 读取

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 数字形式。

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)
);

Pretty Print

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

如果需要自定义字符转义,可以设置 characterEscapes(...)。

var options = new JsonNodeConvertOptions()
    .characterEscapes(myCharacterEscapes);

这个选项直接传给底层 Jackson generator。

Root Value Separator

可以设置根值分隔符。

var options = new JsonNodeConvertOptions()
    .rootValueSeparator(mySeparator);

通常只有在连续写多个 root value 的特殊场景下才需要关注。

highestNonEscapedChar

可以设置最高非转义字符。

var options = new JsonNodeConvertOptions()
    .highestNonEscapedChar(127);

这类配置适合对输出字符集或转义策略有特殊要求的场景。

quoteChar

默认 JSON 字符串和字段名使用双引号。

"

可以通过 quoteChar(...) 修改。

var options = new JsonNodeConvertOptions()
    .quoteChar('\'');

需要注意,修改 quote char 可能会生成非标准 JSON。

Reader / InputStream / File

可以从字符流读取:

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()
);

输出到 Writer / OutputStream / File

输出到字符流:

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));
    }

}

设计说明

1. JsonNodeConverter 是入口

使用者通常只需要直接使用:

new JsonNodeConverter()

不需要直接操作 JsonSerializer 或 JsonDeserializer。

2. 基于 Jackson Core,而不是 Jackson Databind

本模块使用 Jackson Core 的 streaming API。

它不依赖 Jackson Databind,也不使用 Jackson 的 POJO 绑定模型。

转换边界是:

JSON <-> Node

不是:

JSON <-> Java Bean

3. 反序列化容器使用非递归方式

JSON 读取时,对象和数组容器使用栈结构处理。

这样可以避免深层 JSON 读取时直接依赖 Java 方法递归。

但序列化阶段仍然是递归下降方式。

因此仍然需要通过 maxNestingDepth(...) 限制极端深度。

4. 序列化阶段按 Node 类型写出

JsonSerializer 会根据具体 Node 类型调用对应的 JSON 写出方法。

例如:

ObjectNode      writeStartObject / writeName / writeEndObject
ArrayNode       writeStartArray / writeEndArray
StringNode      writeString
IntNode         writeNumber
BooleanNode     writeBoolean
NullNode        writeNull

5. 重复字段策略是 JSON 模块自己的语义

DuplicateFieldPolicy 不是 scx-format 的核心语义。

它只属于 JSON 读取过程。

因为 JSON 对象中重复字段如何处理,并不是所有格式都共有的问题。

6. JsonNodeConvertOptions 是可变配置对象

JsonNodeConvertOptions 的 setter 方法会返回 this。

因此可以链式配置:

var options = new JsonNodeConvertOptions()
    .prettyPrint(true)
    .allowTrailingComma(true)
    .duplicateFieldPolicy(DuplicateFieldPolicy.MERGE);

7. JsonNodeConverter 会复用 Jackson 读写对象

JsonNodeConverter 构造时会创建并复用部分较重对象。

因此推荐复用同一个 JsonNodeConverter 实例,而不是每次转换都创建新实例。

private static final JsonNodeConverter JSON = new JsonNodeConverter();

常见问题

SCX Format JSON 是 JSON 对象绑定库吗?

不是。

它只负责:

JSON <-> Node

如果需要:

Node <-> Java Object

应使用其它对象绑定模块。

它使用 Jackson 吗?

使用。

底层基于 Jackson Core。

它使用 Jackson Databind 吗?

不使用。

它不通过 Jackson Databind 绑定 Java Bean。

默认会格式化输出吗?

不会。

默认 prettyPrint 是 false。

需要格式化时:

new JsonNodeConvertOptions().prettyPrint(true)

支持 JSON 顶层字符串、数字、布尔和 null 吗?

支持。

顶层不要求必须是对象或数组。

遇到重复字段默认怎么处理?

默认使用新值。

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)

如何限制 JSON 最大嵌套深度?

new JsonNodeConvertOptions().maxNestingDepth(200)

formatToNode 会自动关闭我传入的 Reader 吗?

不会。

外部传入资源由调用方关闭。

nodeToFormat 会自动关闭我传入的 Writer 吗?

不会。

外部传入资源由调用方关闭。

JSON 解析失败抛什么异常?

抛 FormatToNodeException。

底层 Jackson 异常会作为 cause 保存。

JSON 输出失败抛什么异常?

抛 NodeToFormatException。

可以输出 UTF-8 字节数组吗?

可以。

byte[] bytes = converter.nodeToFormatBytes(
    node,
    StandardCharsets.UTF_8,
    options
);

可以直接写入文件吗?

可以。

converter.nodeToFormatFile(
    node,
    file,
    StandardCharsets.UTF_8,
    options
);

JSON 和 Node 往返是否稳定?

对于 JSON 能表达的标准数据结构,通常是稳定的。

例如:

JSON -> Node -> JSON -> Node

前后两个 Node 相等。

但如果你开启某些非标准选项,或者输入中包含重复字段,最终结果会受到选项影响。