JSON 模块使用指南
com.alianga.jkit.json 提供完整的 JSON 能力,对外入口集中在 JSON、JSONNode、JSONNodePath 与 JSONSchema 四个类。所有 API 均为零第三方依赖,基于 JDK 标准库实现。
1. 常用对象序列化与反序列化
1.1 序列化
import com.alianga.jkit.json.JSON;
Map<String, Object> map = new HashMap<String, Object>();
map.put("msg", "hello, jkit json !");
map.put("name", "zhangsan");
// 普通序列化
String result = JSON.toJsonString(map);
// {"msg":"hello, jkit json !","name":"zhangsan"}对象、集合、数组、枚举、java.util.Date 以及 java.time 常见类型均可直接序列化。
1.2 反序列化
String json = "{\"msg\":\"hello\",\"name\":\"zhangsan\"}";
// 解析为 Map(默认 LinkedHashMap)
Map<String, Object> map = (Map<String, Object>) JSON.parse(json);
// 指定类型解析
Map<String, Object> map2 = JSON.parseObject(json, Map.class);
User user = JSON.parseObject(json, User.class);
List<User> users = JSON.parseArray("[{\"name\":\"A\"}]", User.class);parseObject / parseArray 用于实体类绑定;parse 用于解析为 Map/List/标量;parseAs 可省去强转。
1.3 读写文件与流
JSON.writeJsonTo(obj, new File("/tmp/test.json")); // 写文件
JSON.writeJsonTo(obj, new FileOutputStream(file)); // 写流
JSON.writeJsonTo(obj, writer); // 写 Writer
Map<String, Object> result = JSON.read(file, Map.class); // 读文件
Map<String, Object> result2 = JSON.read(inputStream, Map.class); // 读流
Map<String, Object> result3 = JSON.read(url, Map.class); // 读远程 URL2. 序列化格式化
通过 WriteOption 或 JSONConfig 控制输出格式。
// 格式化缩进(默认 tab)
JSON.toJsonString(map, WriteOption.FormatOut);
// 冒号后补空格
JSON.toJsonString(map, WriteOption.FormatOut, WriteOption.FormatOutColonSpace);
// 使用 4 个空格缩进
JSON.toJsonString(map, WriteOption.FormatOut, WriteOption.FormatIndentUseSpace);
// 使用 8 个空格缩进
JSON.toJsonString(map, WriteOption.FormatOut, WriteOption.FormatIndentUseSpace8);
// JSONConfig 精确控制(2 空格缩进)
JSONConfig config = JSONConfig.formatOf();
config.setFormatIndentUseSpace(true);
config.setFormatIndentSpaceNum(2);
JSON.toJsonString(map, config);常用 WriteOption:
| 选项 | 说明 |
|---|---|
FormatOut | 格式化缩进输出 |
FormatOutColonSpace | 冒号后补一个空格 |
FormatIndentUseSpace / FormatIndentUseSpace8 | 空格缩进 |
CamelCaseToUnderline | 驼峰属性转下划线输出 |
DateAsTime | 日期序列化为时间戳 |
WriteEnumAsOrdinal | 枚举按 ordinal 输出 |
FullProperty | 输出 null 属性 |
IgnoreNullProperty | 过滤 null 属性(默认) |
BytesArrayToHex / BytesArrayToNative | byte[] 输出方式 |
SkipCircularReference | 跳过循环引用 |
NumberAsString | 大数按字符串输出 |
3. 驼峰 / 下划线自动转换
class User {
private String userName;
private String firstName;
// getter / setter 省略
}
User user = new User();
user.setUserName("tom");
user.setFirstName("Tom");
// 默认输出驼峰:{"userName":"tom","firstName":"Tom"}
JSON.toJsonString(user);
// 转下划线:{"user_name":"tom","first_name":"Tom"}
JSON.toJsonString(user, WriteOption.CamelCaseToUnderline);反序列化时同时支持驼峰与下划线形式(user_name 与 userName 均可绑定到 userName 字段):
User u1 = JSON.parseObject("{\"userName\":\"tom\"}", User.class);
User u2 = JSON.parseObject("{\"user_name\":\"tom\"}", User.class); // 同样生效4. 实体类解析绑定
String json = "{\"name\":\"Tom\",\"age\":18,\"tags\":[\"a\",\"b\"]}";
User user = JSON.parseObject(json, User.class);
// 解析到已有实例(避免反射创建开销)
User target = new User();
JSON.parseToObject(json, target);
// 解析到指定集合
List<User> list = new ArrayList<User>();
JSON.parseToList("[{\"name\":\"A\"}]", list, User.class);@JsonProperty 注解支持:
import com.alianga.jkit.json.annotations.JsonProperty;
class Bean {
@JsonProperty(name = "alias_name") // 指定别名
private String aliasField;
@JsonProperty(serialize = false) // 不序列化
private String skipOnWrite;
@JsonProperty(deserialize = false) // 不反序列化
private String skipOnRead;
}5. 自定义序列化和反序列化
5.1 类型映射器(推荐)
JSONTypeMapper 同时负责序列化与反序列化,注册后全局生效:
JSON.register(Point.class, new JSONTypeMapper<Point>() {
@Override
public Point readOf(Object value) throws Exception {
String[] parts = String.valueOf(value).split(",");
return new Point(Integer.parseInt(parts[0]), Integer.parseInt(parts[1]));
}
@Override
public JSONValue<?> writeAs(Point value, JSONConfig jsonConfig) throws Exception {
return value == null ? null : JSONValue.of(value.x + "," + value.y);
}
});
String out = JSON.toJsonString(new Point(3, 4)); // "3,4"
Point p = JSON.parseObject("\"10,20\"", Point.class);5.2 字段级映射器
JSONTypeFieldMapper 配合 @JsonProperty(mapper = ...) 只作用于指定字段:
class SecretBean {
@JsonProperty(mapper = ReverseMapper.class)
private String secret;
}
class ReverseMapper extends JSONTypeFieldMapper<String> {
@Override
public String readOf(Object value) throws Exception {
return new StringBuilder(String.valueOf(value)).reverse().toString();
}
@Override
public JSONValue<?> writeAs(String value, JSONConfig jsonConfig) throws Exception {
return JSONValue.of(new StringBuilder(value).reverse().toString());
}
}5.3 底层序列化器 / 反序列化器
JSON.register(MyType.class, new JSONTypeSerializer() { ... }); // 仅序列化
JSON.register(MyType.class, new JSONTypeDeserializer() { ... }); // 仅反序列化
JSON.register(MyType.class, mapper); // 二者合一6. JSONNode:节点树按需解析
JSONNode 适合大文本 JSON 的按需访问:扫描一次、仅解析访问到的路径,无需解析完整 JSON。
String json = "{\"name\":\"Tom\",\"dept\":{\"id\":7,\"name\":\"dev\"}}";
JSONNode root = JSONNode.parse(json);
// 通过路径获取值(/ 分隔,数组用下标)
String name = root.getPathValue("/name", String.class);
int id = root.getPathValue("/dept/id", int.class);
// 定位节点
JSONNode dept = root.get("/dept");
JSONNode idNode = root.get("dept/id");
// 直接取值 / 转类型
String s = root.getChildValue("name", String.class);
Map<String, Object> map = root.asMap();
List<Object> list = root.get("/tags").asList();
// 转实体 / 集合
User user = root.toBean(User.class);
List<User> users = root.get("/list").toList(User.class);6.1 局部提取(from)
from 只扫描到目标路径即返回,比 parse 更高效,支持懒加载:
JSONNode bookRoot = JSONNode.from(json, "/store/book");
JSONNode title = JSONNode.from(json, "/store/book/0/title");
// 提取并直接转化类型
Double price = JSONNode.from(json, "/store/book/0/price", Double.class);6.2 节点修改
root.setPathValue("/dept/name", "newdev"); // 按路径设值
root.setChildValue("name", "Jerry"); // 设子值
root.setChildValue("extra", "v", true); // 不存在则创建
root.removeField("dept"); // 删除字段
root.get("/arr").removeElementAt(0); // 删除数组元素6.3 聚合与 diff
JSONNode arr = JSONNode.parse("[{\"price\":8.95},{\"price\":12.99}]");
arr.max("price"); // 12.99
arr.min("price"); // 8.95
arr.avg("price"); // 平均值
JSONNode[] diff = JSONNode.diff("{\"a\":1}", "{\"a\":2}");7. JSON xpath 提取
7.1 通配与下标
// 提取所有 author
List<JSONNode> authors = JSONNode.extract(json, "/store/book/*/author");
// 指定下标
JSONNode author = JSONNode.extract(json, "/store/book/1/author").get(0);
// 前 2 个(下标 1- 包含 1)
List<JSONNode> top2 = JSONNode.extract(json, "/store/book/1-/author");
// 从下标 1 开始(1+ 包含 1)
List<JSONNode> from1 = JSONNode.extract(json, "/store/book/1+/author");
// 离散下标(编程式构建)
JSONNodePath path = JSONNodePath.create()
.exact("store").exact("book").indexs(0, 2).exact("author");
List<JSONNode> selected = path.collect(JSONNode.parse(json));7.2 递归查找(collect)
// // 递归查找所有 name
List<JSONNode> names = JSONNode.collect(json, "//name");
// 递归 + 下标
List<JSONNode> authors = JSONNode.collect(json, "//book/1+/author");7.3 属性过滤
过滤表达式以 *[表达式] 形式紧跟路径片段,字符串使用单引号:
// author == 'Nigel Rees'
List<JSONNode> matched = JSONNode.collect(json, "/store/book/*[author == 'Nigel Rees']/title");7.4 首尾与类型转化
String first = JSONNode.first(json, "/store/book/*/author", String.class);
String last = JSONNode.last(json, "/store/book/*/author", String.class);
String fallback = JSONNode.firstIfEmpty(json, "/none/*/x", String.class, "default");8. JSON Schema 校验
String schemaJson = "{\n"
+ " \"type\": \"object\",\n"
+ " \"properties\": {\n"
+ " \"name\": {\n"
+ " \"must\": true,\n"
+ " \"type\": \"string\",\n"
+ " \"rules\": [\n"
+ " {\"expression\": \"value.indexOf('test') > -1\", \"message\": \"必须包含test\"}\n"
+ " ]\n"
+ " },\n"
+ " \"age\": {\"type\": \"number\", \"minimum\": 20, \"maximum\": 100},\n"
+ " \"key\": {\"type\": [\"number\", \"boolean\"]}\n"
+ " }\n"
+ "}";
JSONSchema schema = JSONSchema.of(schemaJson);
JSONSchemaResult result = schema.validate("{\"name\":\"test user\",\"age\":33,\"key\":false}");
result.isSuccess(); // true
result.getMessage(); // 失败信息
result.getPath(); // 失败路径支持的约束关键字:
| 关键字 | 说明 |
|---|---|
type | 类型:object / array / string / number / integer / boolean / null,支持数组多类型 |
required | 标准必填写法,字符串数组,写在父级:"required": ["name", "age"]。只要字段存在即满足,值为 null 也算存在 |
must | 本库自有必填写法,布尔值,写在字段自己的 schema 里:"name": {"must": true}。要求字段存在且值不为 null |
minimum / maximum | 数值边界 |
exclusiveMinimum / exclusiveMaximum | 开区间 |
minLength / maxLength | 字符串长度 |
pattern | 字符串正则 |
minItems / maxItems | 数组长度 |
items | 数组元素 schema |
enum | 类型枚举 |
format | url / email / date |
rules | 自定义规则,支持 regular 正则与 expression 表达式 |
anyOf / allOf / oneOf | 组合校验 |
disableExtra | 禁止出现未定义字段 |
必填:required 与 must 选哪个
两种写法都认,语义不同,混用时都要满足:
// 标准写法:父级声明,只要求字段存在
JSONSchema.of("{\"type\":\"object\",\"required\":[\"name\"],\"properties\":{\"name\":{\"type\":\"string\"}}}")
.validateSuccess("{}"); // false:field 'name' is required but not found
// 自有写法:字段内声明,还要求值不是 null
JSONSchema.of("{\"type\":\"object\",\"properties\":{\"name\":{\"type\":\"string\",\"must\":true}}}")
.validateSuccess("{\"name\":null}"); // false接口入参校验推荐用 required(与标准 schema 工具互通),must 适合「不能传 null」的场景。required 列出的字段若还声明了 type,{"v":null} 会因类型不匹配失败——那是 type 的判定,不是必填判定。
9. JSON Patch(RFC 6902)
对一份 JSON 文档施加一组操作(补丁),得到修改后的文档。补丁本身是一个 JSON 数组,每个元素是 一个带 op 与 path 字段的操作对象。适用于「只更新大文档里的一小块」「把服务端下发的增量合并到 本地缓存」等场景——不必传整份文档。
支持的操作:
| op | 字段 | 说明 |
|---|---|---|
add | path + value | 在 path 处设值;父是对象则新增/覆盖键,父是数组则按下标插入(- 或下标等于长度时追加到末尾) |
remove | path | 删除 path 处的成员;数组按下标删除并前移 |
replace | path + value | 替换已存在的成员(path 不存在则报错) |
move | from + path | 先 remove from,再 add 到 path |
copy | from + path | 深拷贝 from 处的值再 add 到 path |
test | path + value | 断言 path 处的值与 value 相等,不等则抛出 JSONPatchException |
path 是 JSON Pointer(RFC 6901):以 / 分隔的引用记号,空串表示根文档;数组下标是十进制非负整数(0 之外不允许前导零、不允许 +),- 表示数组末尾(仅 add 可用);记号里的 ~1 解码为 /、~0 解码为 ~(用来表达本来含 / 或 ~ 的键名)。 test 对数字按值精确比较(BigDecimal 语义),所以 1 与 1.0 视为相等,而 9007199254740993 与 9007199254740992 不相等——大整数不会被 double 截断成假相等。
// 文本入口:自行解析与序列化
String patched = JSONPatch.apply(
"{\"title\":\"老标题\",\"tags\":[\"a\"]}",
"[{\"op\":\"replace\",\"path\":\"/title\",\"value\":\"新标题\"},"
+ " {\"op\":\"add\",\"path\":\"/tags/-\",\"value\":\"b\"}]");
// -> {"title":"新标题","tags":["a","b"]}
// 对象入口:对已解析的 Map/List 原地修改,返回同一棵树的引用
Object doc = JSON.parse("{\"a\":1}");
JSONPatch.apply(doc, JSON.parse("[{\"op\":\"add\",\"path\":\"/b\",\"value\":2}]"));注意:
JSONPatch.apply(Object, ...)会在传入的文档树上原地修改,调用方应使用返回值;copy会深拷贝源值,避免两边共享同一引用。path非法、下标越界、操作缺失字段、 删除/替换不存在的成员或test不成立时,抛出JSONPatchException(JSONException的子类)。
10. 其它能力
// NDJSON(JSON Lines)
String nd = JSON.toNdJsonString(Arrays.asList(obj1, obj2));
List parsed = JSON.parseNdJson(nd);
// 排序
String sorted = JSON.sortJsonString("{\"b\":2,\"a\":1}");
// 校验 JSON 合法性
boolean valid = JSON.validate(json);
// 深拷贝
User copy = JSON.cloneObject(user);
// 非标准 JSON 兼容(注释、单引号、无引号 key、尾逗号)
JSON.parse(json, ReadOption.AllowComment,
ReadOption.AllowSingleQuotes,
ReadOption.AllowUnquotedFieldNames,
ReadOption.AllowLastEndComma);完整测试用例见 jkit-core/src/test/java/com/alianga/jkit/json/JSONTest.java、JSONNodeTest.java 与 JSONSchemaTest.java。