本文深度剖析 Hutool 中 JSON 字符串校验的常见陷阱,提供源码级解析,并给出一个功能完备的 JSON 校验工具类。
在现代 Java 开发中,JSON 作为数据交换的标准格式无处不在。Hutool 作为 Java 工具库的瑞士军刀,其 JSONUtil 类提供了便捷的 JSON 操作功能。然而,许多开发者在判断字符串是否为有效 JSON 时,常常掉入一些陷阱,导致潜在的数据解析错误和安全问题。
本文将深入剖析 Hutool 中 JSON 校验的常见问题,解读源码实现,并提供一个功能完备的 JSON 校验工具类,帮助开发者避免常见陷阱。
isTypeJSON():宽松但不可靠的校验源码实现(Hutool 5.8.16):
public static boolean isTypeJSON(String str) {
if (StrUtil.isBlank(str)) {
return false;
}
str = str.trim();
return (str.startsWith("{") && str.endsWith("}"))
|| (str.startsWith("[") && str.endsWith("]"));
}
问题分析:
"{ invalid json }" 会被识别为有效 JSON"123"、"true" 等合法 JSON 值会被拒绝isJson():严格但有限制的校验源码实现:
public static boolean isJson(String str) {
return isJsonObj(str) || isJsonArray(str);
}
public static boolean isJsonObj(String str) {
if (StrUtil.isBlank(str)) {
return false;
}
return JSON_TYPE_OBJECT == getJSONType(str);
}
public static boolean isJsonArray(String str) {
if (StrUtil.isBlank(str)) {
return false;
}
return JSON_TYPE_ARRAY == getJSONType(str);
}
深入解析:
getJSONType() 方法尝试解析字符串JSONParser 进行解析,失败则抛出异常潜在问题:
// 以下合法 JSON 会被 isJson() 拒绝
JSONUtil.isJson("\"string\""); // false
JSONUtil.isJson("123"); // false
JSONUtil.isJson("true"); // false
JSONUtil.isJson("null"); // false
// JavaScript 对象(合法)
const jsObj = {
key: 'value',
trailing: true, // 尾随逗号
// 注释
};
// 严格 JSON(非法)
String jsonStr = "{'key': 'value', 'trailing': true, }";
Hutool 的严格校验会拒绝此类格式,但开发者常误以为它们是合法的 JSON。
JSONUtil.isJson(" "); // false(正确)
JSONUtil.isJson("null"); // true(正确,但常被误解)
JSONUtil.isJson(""); // false(正确)
许多开发者期望空字符串被视为 "空 JSON",但这不符合规范。
// 未转义的控制字符
String invalid = "{\"key\": \"value\n\"}";
// 正确的转义形式
String valid = "{\"key\": \"value\\n\"}";
Hutool 的严格校验会拒绝未转义的特殊字符,但错误信息可能不够明确。
基于以上分析,我开发了一个功能完备的 JSON 校验工具类,解决 Hutool 的局限性:
import cn.hutool.json.JSON;
import cn.hutool.json.JSONConfig;
import cn.hutool.json.JSONException;
import cn.hutool.json.JSONUtil;
public class JsonValidator {
/**
* 快速格式检查(类似 isTypeJSON)
* 仅检查首尾字符,不验证内容
*/
public static boolean isJsonLike(String str) {
if (str == null || str.isBlank()) {
return false;
}
String trimmed = str.trim();
return (trimmed.startsWith("{") && trimmed.endsWith("}")) ||
(trimmed.startsWith("[") && trimmed.endsWith("]"));
}
/**
* 严格JSON验证(符合RFC标准)
* 支持所有JSON类型:对象、数组、字符串、数字、布尔、null
*/
public static boolean isValidJson(String str) {
if (str == null || str.isBlank()) {
return false;
}
try {
parseAny(str);
return true;
} catch (JSONException e) {
return false;
}
}
/**
* 严格JSON对象验证
*/
public static boolean isValidJsonObject(String str) {
if (str == null || str.isBlank()) {
return false;
}
try {
JSONUtil.parseObj(str);
return true;
} catch (JSONException e) {
return false;
}
}
/**
* 严格JSON数组验证
*/
public static boolean isValidJsonArray(String str) {
if (str == null || str.isBlank()) {
return false;
}
try {
JSONUtil.parseArray(str);
return true;
} catch (JSONException e) {
return false;
}
}
/**
* 宽松JSON验证(支持JavaScript风格)
* 允许:单引号、尾随逗号、注释(需配置)
*/
public static boolean isLenientJson(String str) {
if (str == null || str.isBlank()) {
return false;
}
try {
// 创建宽松配置
JSONConfig config = JSONConfig.create()
.setCheckDuplicate(false) // 不检查重复键
.setStripTrailingZeros(false)
.setOrder(false)
.setIgnoreNullValue(false)
.setDateFormat("yyyy-MM-dd HH:mm:ss");
JSONUtil.parse(str, config);
return true;
} catch (JSONException e) {
return false;
}
}
/**
* 解析任意JSON类型(修复Hutool的局限性)
*/
private static Object parseAny(String jsonStr) {
String trimmed = jsonStr.trim();
if (trimmed.startsWith("{") && trimmed.endsWith("}")) {
return JSONUtil.parseObj(jsonStr);
} else if (trimmed.startsWith("[") && trimmed.endsWith("]")) {
return JSONUtil.parseArray(jsonStr);
} else if (trimmed.startsWith("\"") && trimmed.endsWith("\"")) {
return JSONUtil.parse(jsonStr); // 字符串
} else if ("null".equals(trimmed)) {
return null;
} else if ("true".equals(trimmed) || "false".equals(trimmed)) {
return Boolean.parseBoolean(trimmed);
}
// 尝试解析数字
try {
return Double.parseDouble(trimmed);
} catch (NumberFormatException e) {
throw new JSONException("Invalid JSON format");
}
}
/**
* 获取JSON校验的详细结果
* @return 包含校验状态和错误信息的对象
*/
public static ValidationResult validateJson(String str) {
if (str == null) {
return new ValidationResult(false, "Input is null");
}
if (str.isBlank()) {
return new ValidationResult(false, "Input is blank");
}
try {
parseAny(str);
return new ValidationResult(true, "Valid JSON");
} catch (JSONException e) {
return new ValidationResult(false, "JSON parse error: " + e.getMessage());
}
}
public static class ValidationResult {
private final boolean valid;
private final String message;
public ValidationResult(boolean valid, String message) {
this.valid = valid;
this.message = message;
}
public boolean isValid() {
return valid;
}
public String getMessage() {
return message;
}
}
}
| 方法名 | 校验级别 | 特点 | 适用场景 |
|---|---|---|---|
isJsonLike() |
格式检查 | 快速但不可靠 | 日志过滤、初步筛查 |
isValidJson() |
严格RFC校验 | 符合JSON标准 | API输入验证、数据交换 |
isValidJsonObject() |
严格对象校验 | 确保是JSON对象 | 配置文件解析 |
isValidJsonArray() |
严格数组校验 | 确保是JSON数组 | 批量数据处理 |
isLenientJson() |
宽松校验 | 支持JS风格扩展 | 前端输入、配置文件 |
validateJson() |
详细校验 | 返回校验结果和错误信息 | 调试、错误报告 |
任意类型解析:
private static Object parseAny(String jsonStr) {
// 根据首尾字符判断类型
if (trimmed.startsWith("{") && trimmed.endsWith("}")) {
// 解析对象
} else if (trimmed.startsWith("[") && trimmed.endsWith("]")) {
// 解析数组
} else if (trimmed.startsWith("\"") && trimmed.endsWith("\"")) {
// 解析字符串
} else if ("null".equals(trimmed)) {
// 解析null
}
// ...其他类型
}
详细错误报告:
public static ValidationResult validateJson(String str) {
try {
parseAny(str);
return new ValidationResult(true, "Valid JSON");
} catch (JSONException e) {
return new ValidationResult(false,
"JSON parse error: " + e.getMessage());
}
}
灵活的宽松配置:
JSONConfig config = JSONConfig.create()
.setCheckDuplicate(false) // 允许重复键
.setStripTrailingZeros(false)
.setOrder(false) // 不保持键顺序
.setIgnoreNullValue(false) // 包含null值
.setDateFormat("yyyy-MM-dd HH:mm:ss"); // 自定义日期格式
API 接口输入校验:
public ResponseEntity<?> handleRequest(@RequestBody String body) {
if (!JsonValidator.isValidJsonObject(body)) {
return ResponseEntity.badRequest().body("Invalid JSON format");
}
// 处理逻辑
}
配置文件加载:
public void loadConfig(String configPath) {
String configContent = FileUtil.readUtf8String(configPath);
if (JsonValidator.isLenientJson(configContent)) {
JSONConfig config = createLenientConfig();
JSONObject configObj = JSONUtil.parseObj(configContent, config);
// 应用配置
} else {
throw new ConfigException("Invalid configuration format");
}
}
数据管道处理:
public void processData(Stream<String> dataStream) {
dataStream.filter(JsonValidator::isValidJson)
.map(JSONUtil::parseObj)
.forEach(this::processItem);
}
对于高频调用的场景,可以采用分层校验策略:
public boolean isJsonEfficient(String str) {
// 第一层:快速检查
if (!JsonValidator.isJsonLike(str)) {
return false;
}
// 第二层:结构检查
if (!hasBasicJsonStructure(str)) {
return false;
}
// 第三层:完整解析
return JsonValidator.isValidJson(str);
}
private boolean hasBasicJsonStructure(String str) {
String trimmed = str.trim();
if (trimmed.startsWith("{")) {
// 检查对象至少包含一个键值对
return trimmed.indexOf(':') > 0 &&
trimmed.indexOf(':') < trimmed.length() - 1;
} else if (trimmed.startsWith("[")) {
// 检查数组至少包含一个元素
return trimmed.length() > 2 &&
trimmed.indexOf(',') > 0;
}
return false;
}
完整的 JSONValidator 工具类已在 GitHub Gist 上开源:https://gist.github.com/example/json-validator
通过本文的剖析和提供的工具类,希望能帮助开发者避免 JSON 校验中的常见陷阱,构建更加健壮的 JSON 处理逻辑。