作为 C++ 开发者,我们常面临 JSON 数据校验的痛点 —— 接口对接时字段类型不匹配、必填参数缺失、嵌套结构异常等问题,往往需要手写大量冗余校验代码。而 JSON Schema 作为 JSON 数据的 “契约协议”,能标准化地定义数据结构、约束条件和校验规则,让数据校验更高效、可维护。本文基于 JSON Schema 最新 2020-12 版本规范,从核心概念、语法细节到 C++ 实战落地,带你全面掌握这一工具。
一、JSON Schema 核心认知
1. 什么是 JSON Schema?
JSON Schema 是一种描述 JSON 数据结构和约束的 JSON 文档,本质是 “JSON 的元数据”。它能:
- 定义 JSON 数据的合法字段名、数据类型(字符串 / 数字 / 对象等);
- 限制字段取值范围(如数值区间、字符串长度、枚举值);
- 规定必填 / 可选字段、默认值;
- 支持嵌套结构、数组元素校验;
- 提供自描述能力(通过元数据说明字段含义)。
对于 C++ 开发者而言,JSON Schema 的核心价值是解耦校验逻辑与业务代码—— 无需手写if-else判断字段合法性,只需通过 Schema 定义规则,配合校验库即可自动完成校验。
2. 版本演进与选择
JSON Schema 的版本采用 “日期型标识符”(2020-12、2019-09),替代了早期的 “Draft-N” 命名(如 Draft-07)。关键版本差异如下:
| 版本 | 核心改进 | 适用场景 |
|---|---|---|
| 2020-12(当前稳定版) | 简化元数据引用、增强数组校验、新增unevaluated关键字、优化格式校验 |
新项目首选,支持更灵活的复杂结构校验 |
| 2019-09 | 首次引入日期版本号、拆分核心与校验模块 | 过渡版本,可平滑迁移至 2020-12 |
| Draft-07 | 广泛兼容旧版校验库 | legacy 项目维护,不推荐新项目使用 |
建议:新开发的 C++ 项目直接采用 2020-12 版本,享受更完善的语法特性和更好的工具支持。
3. 规范结构拆解
JSON Schema 规范分为 3 个核心部分,对应 C++ 开发者需要关注的核心能力:
| 规范模块 | 核心作用 | 对 C++ 的意义 |
|---|---|---|
| JSON Schema Core | 定义 Schema 的基础语法(如$schema、$id、$ref) |
理解 Schema 文档的结构规范,正确引用和复用 Schema |
| JSON Schema Validation | 定义校验关键字(如type、required、minLength) |
掌握如何编写校验规则,满足业务数据约束 |
| Relative JSON Pointers | 扩展 JSON Pointer 语法,支持相对路径引用 | 复杂嵌套结构中,精准引用其他字段的值(如数组元素依赖) |
二、JSON Schema 2020-12 核心语法详解
1. 基础结构:一个完整的 Schema 示例
先看一个 C++ 接口常用的 “用户登录请求” Schema 示例,直观理解核心语法:
{
"$schema": "http://json-schema.org/draft/2020-12/schema", // 声明Schema版本(必填)
"$id": "https://example.***/login-request-schema", // Schema唯一标识(可选,用于复用)
"title": "用户登录请求数据", // 文档说明(可选)
"description": "C++后端接收的用户登录请求JSON格式约束", // 详细描述(可选)
"type": "object", // 根数据类型为对象
"required": ["username", "password"], // 必填字段
"properties": { // 字段定义
"username": {
"type": "string", // 字符串类型
"minLength": 3, // 最小长度3
"maxLength": 20, // 最大长度20
"description": "登录用户名(3-20位字母/数字)"
},
"password": {
"type": "string",
"minLength": 6,
"maxLength": 32,
"format": "password", // 格式标注(仅提示,默认不校验,需开启断言)
"description": "登录密码(6-32位)"
},
"remember_me": {
"type": "boolean",
"default": false, // 默认值
"description": "是否记住登录状态(默认false)"
},
"login_type": {
"type": "string",
"enum": ["password", "sms", "wechat"], // 枚举值约束
"default": "password"
}
},
"additionalProperties": false // 不允许出现Schema中未定义的字段
}
2. 核心关键字详解(C++ 开发高频使用)
(1)基础标识关键字
-
$schema:必填,声明 Schema 遵循的版本规范,2020-12 版本固定值为http://json-schema.org/draft/2020-12/schema。作用:告诉校验库如何解析 Schema 语法,避免版本兼容问题。 -
$id:可选,Schema 的唯一 URI 标识,用于跨文档引用(如复用其他 Schema 中的规则)。示例:"$id": "https://example.***/***mon-schema",后续可通过$ref引用。 -
title/description:可选,文档说明字段,增强 Schema 可读性(C++ 团队协作时建议必填)。
(2)数据类型关键字 type
定义字段的 JSON 数据类型,C++ 开发者需注意 JSON 类型与 C++ 类型的映射关系:
| JSON 类型 | 关键字取值 | C++ 对应类型 | 说明 |
|---|---|---|---|
| 字符串 | "type": "string" |
std::string |
包含普通字符串、日期、密码等 |
| 数字 | "type": "number" |
double/float
|
支持小数 |
| 整数 | "type": "integer" |
int/long/uint64_t
|
仅整数(2020-12 明确区分 number 和 integer) |
| 布尔 | "type": "boolean" |
bool |
true/false
|
| 对象 | "type": "object" |
nlohmann::json::object_t(或自定义结构体) |
嵌套字段通过properties定义 |
| 数组 | "type": "array" |
std::vector/nlohmann::json::array_t
|
元素约束通过items定义 |
| 空值 | "type": "null" |
std::nullptr_t |
极少使用,通常用于可选字段默认值 |
多类型支持:字段允许多种类型时,用数组表示,例如:
"age": {
"type": ["integer", "null"], // 允许整数或null
"minimum": 0
}
(3)对象类型约束关键字(!!!)
{
"type": "object", // 必须是对象类型
"properties": { // 定义对象允许的字段列表
"username": { // 用户名字段
"type": "string", // 必须是字符串类型
"minLength": 3, // 最少3个字符
"maxLength": 20 // 最多20个字符
},
"password": { // 密码字段
"type": "string", // 必须是字符串类型
"minLength": 6 // 最少6个字符,确保安全性
},
"age": { // 年龄字段(可选)
"type": "integer", // 必须是整数类型
"minimum": 0, // 最小值为0
"maximum": 150 // 最大值为150(合理年龄范围)
}
},
"required": ["username", "password"], // 必须包含的字段:用户名和密码
"additionalProperties": false, // 禁止额外字段(提高安全性)
"minProperties": 2, // 对象至少要有2个字段
"maxProperties": 3 // 对象最多只能有3个字段
}
当type为object时,通过以下关键字约束字段结构:
properties:核心,定义对象的字段列表,每个字段对应一个子 Schema。
required:数组类型,指定必填字段(C++ 接口中必填参数必须在此声明)。示例:"required": ["username", "password"],缺少任一字段则校验失败。
{
"username": "john_doe" // 只有用户名
// ❌ 错误:缺少必填字段 "password"
// ❌ 错误:字段数量只有1个,但至少需要2个(minProperties)
}
{
"username": "john_doe",
"password": "123456",
"age": 25,
"email": "john@example.***" // ❌ 错误:额外字段不被允许
// additionalProperties: false 禁止任何未在properties中定义的字段
}
{
"username": "john_doe"
// ❌ 错误:只有1个字段,但minProperties要求至少2个字段
// ❌ 错误:缺少必填字段 "password"
}
{
"username": "john_doe",
"password": "123456",
"age": "twenty-five" // ❌ 错误:age应该是integer,但这里是string
}
additionalProperties:布尔值或子 Schema,控制是否允许未定义字段。
-
false:不允许额外字段(推荐,避免接口接收无效数据); -
true:允许额外字段(不推荐,可能引入脏数据); - 子 Schema:额外字段需满足该 Schema 约束(如
"additionalProperties": {"type": "string"})。
minProperties/maxProperties:限制对象的字段数量(如"minProperties": 2表示至少 2 个字段)。
{
"username": "alice", // 提供用户名(必填)
"password": "secret123", // 提供密码(必填)
"age": 25 // 提供年龄(可选)
// ✅ 满足:有3个字段,包含所有必填字段,没有额外字段
}
(4)数组类型约束关键字
当type为array时,约束数组元素和长度:
items:子 Schema 或 Schema 数组,定义数组元素的规则。
示例 1:所有元素为整数且大于 0:
"tags": {
"type": "array",
"items": {
"type": "integer",
"minimum": 1
}
}
示例 2:数组元素按位置匹配不同 Schema(tuple 模式):
"coordinate": {
"type": "array",
"items": [
{"type": "number", "description": "经度"},
{"type": "number", "description": "纬度"}
],
"minItems": 2,
"maxItems": 2 // 固定长度2,对应经纬度
}
{
"type": "array", // 必须是数组类型
"items": {
"type": "string" // 数组元素必须是字符串
},
"minItems": 1, // 数组至少要有1个元素(不能为空数组)
"maxItems": 5, // 数组最多只能有5个元素
"uniqueItems": true, // 数组元素必须唯一(不能重复)
"unevaluatedItems": false // 禁止额外元素(所有元素都必须通过items校验)
}
minItems/maxItems:限制数组长度(如"minItems": 1表示数组不能为空)。
uniqueItems:布尔值,是否要求数组元素唯一(如"uniqueItems": true表示无重复元素)。
unevaluatedItems:2020-12 新增,控制是否允许未被items校验的元素(如"unevaluatedItems": false禁止额外元素)。
校验:
["apple"]
// ✅ 满足:有1个元素(满足minItems),元素是字符串,没有重复
["apple", "banana", "cherry"]
// ✅ 满足:有3个元素,都是字符串,元素唯一,没有额外元素
["apple", "banana", "cherry", "date", "elderberry"]
// ✅ 满足:有5个元素(满足maxItems),都是字符串,元素唯一
[]
// ❌ 错误:数组为空,但minItems要求至少1个元素
["apple", "banana", "cherry", "date", "elderberry", "fig"]
// ❌ 错误:有6个元素,但maxItems限制最多5个元素
["apple", "banana", "apple"]
// ❌ 错误:元素"apple"重复,但uniqueItems要求元素唯一
["apple", 123, "banana"]
// ❌ 错误:第2个元素是数字,但items要求所有元素都是字符串
// unevaluatedItems: false 会拒绝任何不满足items约束的元素
(5)数值类型约束关键字
适用于type为number或integer的字段:
-
minimum/maximum:数值最小值 / 最大值(包含边界,如"minimum": 0允许 0)。 -
exclusiveMinimum/exclusiveMaximum:数值最小值 / 最大值(不包含边界,如"exclusiveMinimum": 0不允许 0)。 -
multipleOf:数值必须是该值的整数倍(如"multipleOf": 5,允许 5、10、15 等)。
(6)字符类型约束关键字
适用于type为string的字段:
minLength/maxLength:字符串长度范围(字符数,不是字节数,UTF-8 编码需注意)。
pattern:正则表达式约束(如手机号校验:"pattern": "^1[3-9]\\d{9}$")。注意:JSON 中反斜杠需转义,所以正则中的\d需写成\\d。
format:字符串格式标注,2020-12 将其拆分为 “注解型” 和 “断言型”:
- 注解型(仅说明,默认不校验):
date(日期,YYYY-MM-DD)、time(时间,HH:MM:SS)、email(邮箱)、uri(URI)、password(密码)等; - 断言型(需开启校验,需校验库支持):需引入
Format Assertion Vocabulary,例如:{ "$schema": "http://json-schema.org/draft/2020-12/schema", "$vocabulary": { "http://json-schema.org/draft/2020-12/vocab/format-assertion": true }, "properties": { "email": { "type": "string", "format": "email" // 开启邮箱格式校验 } } }
(7)枚举与默认值
-
enum:限制字段取值为枚举列表中的值(支持任意 JSON 类型),例如:"login_type": { "type": "string", "enum": ["password", "sms", "wechat"] // 仅允许这三个值 } -
default:字段默认值(当 JSON 中缺少该字段时,校验库可能返回默认值,需看库实现)。
(8)引用与复用关键字 $ref(!!!)
核心复用机制,C++ 开发中可用于抽取公共 Schema(如分页参数、错误码定义),减少冗余。
语法:"$ref": "URI",URI 可分为:
- 绝对 URI:引用外部 Schema,例如
"$ref": "https://example.***/***mon-schema#/properties/page"; - 相对 URI:引用当前 Schema 中的规则,例如
"$ref": "#/definitions/phone"(#表示当前文档根路径)。
示例:抽取公共字段复用
{
"$schema": "http://json-schema.org/draft/2020-12/schema",
"definitions": { // 定义可复用的规则
"page_param": {
"type": "object",
"properties": {
"page_num": { "type": "integer", "minimum": 1, "default": 1 },
"page_size": { "type": "integer", "minimum": 1, "maximum": 100, "default": 20 }
}
}
},
// 引用公共分页参数
"allOf": [ // 合并多个Schema规则
{ "$ref": "#/definitions/page_param" },
{
"properties": {
"keyword": { "type": "string", "minLength": 1 }
},
"required": ["keyword"]
}
]
}
3. 高级特性:组合与条件校验
(1)组合关键字(合并多个 Schema)
-
allOf:字段必须满足所有子 Schema(逻辑与),例如:allOf: [Schema1, Schema2]; -
anyOf:字段至少满足一个子 Schema(逻辑或),例如:anyOf: [{"type": "string"}, {"type": "integer"}]; -
oneOf:字段恰好满足一个子 Schema(互斥或),例如:oneOf: [{"type": "string", "enum": ["a"]}, {"type": "integer", "enum": [1]}]; -
not:字段必须不满足子 Schema(逻辑非),例如:not: {"type": "string"}(不能是字符串)。
(2)条件校验关键字
-
if/then/else:满足if的条件时,执行then的校验;否则执行else的校验。示例:当login_type为sms时,必须传入sms_code字段:{ "type": "object", "properties": { "login_type": { "type": "string", "enum": ["password", "sms"] }, "sms_code": { "type": "string", "length": 6 } }, "if": { "properties": { "login_type": { "const": "sms" } } // const:精确匹配值 }, "then": { "required": ["sms_code"] }, // 满足if时,sms_code必填 "else": { "not": { "required": ["sms_code"] } } // 不满足时,禁止sms_code }
三、C++ 实战:JSON Schema 校验落地
1. 选型:C++ JSON Schema 校验库
C++ 生态中支持 JSON Schema 2020-12 的主流库:
| 库名 | 支持版本 | 依赖 | 优势 | 劣势 |
|---|---|---|---|---|
| nlohmann/json-schema-validator | 2020-12/2019-09/Draft-07 | nlohmann/json(C++11+) | 轻量、API 简洁、与 nlohmann/json 无缝集成(C++ 开发者最常用 JSON 库) | 部分高级特性(如 Relative JSON Pointers)支持有限 |
| rapidjson-schema | Draft-04/06/07(2020-12 部分支持) | rapidjson(C++11+) | 性能极致、内存占用低 | 2020-12 特性支持不全,API 较繁琐 |
| jsoncons/jsoncons | 2020-12/2019-09/Draft-07 | 无外部依赖(C++11+) | 全特性支持、跨平台性好 | 生态相对小众,文档较少 |
推荐选型:优先使用 nlohmann/json-schema-validator,原因是:
- 与 C++ 最流行的 JSON 库
nlohmann/json深度集成,无需额外适配; - 完美支持 2020-12 核心特性,满足 99% 的业务场景;
- API 直观,上手成本低。
2. 环境搭建(以 nlohmann 方案为例)
(1)安装依赖
- 安装
nlohmann/json:通过 CMake FetchContent 引入,或直接下载头文件放入项目:// CMakeLists.txt include(FetchContent) FetchContent_Declare( nlohmann_json GIT_REPOSITORY https://github.***/nlohmann/json.git GIT_TAG v3.11.3 ) FetchContent_MakeAvailable(nlohmann_json) - 安装
json-schema-validator:同样通过 FetchContent 引入:FetchContent_Declare( json_schema_validator GIT_REPOSITORY https://github.***/pboettch/json-schema-validator.git GIT_TAG v2.2.0 ) FetchContent_MakeAvailable(json_schema_validator)
(2)项目配置
// CMakeLists.txt
add_executable(json_schema_demo main.cpp)
target_link_libraries(json_schema_demo
PRIVATE nlohmann_json::nlohmann_json
PRIVATE nlohmann_json_schema_validator
)
target_***pile_features(json_schema_demo PRIVATE cxx_std_17) // 建议C++17+
3. 实战代码:校验用户登录请求
(1)步骤拆解
- 定义 JSON Schema(2020-12 版本);
- 读取待校验的 JSON 数据(模拟 C++ 后端接收的 HTTP 请求体);
- 初始化校验器,加载 Schema;
- 执行校验,处理校验结果(成功 / 失败信息)。
(2)完整代码
#include <iostream>
#include <nlohmann/json.hpp>
#include <nlohmann/json-schema.hpp>
using json = nlohmann::json;
using validator = nlohmann::json_schema::validator;
int main() {
// 1. 定义JSON Schema(2020-12版本)
const json login_schema = R"(
{
"$schema": "http://json-schema.org/draft/2020-12/schema",
"$id": "https://example.***/login-request-schema",
"title": "用户登录请求",
"type": "object",
"required": ["username", "password"],
"properties": {
"username": {
"type": "string",
"minLength": 3,
"maxLength": 20,
"pattern": "^[a-zA-Z0-9]+$" // 仅允许字母数字
},
"password": {
"type": "string",
"minLength": 6,
"maxLength": 32
},
"remember_me": {
"type": "boolean",
"default": false
},
"login_type": {
"type": "string",
"enum": ["password", "sms", "wechat"],
"default": "password"
},
"sms_code": {
"type": "string",
"minLength": 6,
"maxLength": 6
}
},
"additionalProperties": false,
// 条件校验:login_type为sms时,sms_code必填
"if": {
"properties": {
"login_type": { "const": "sms" }
}
},
"then": {
"required": ["sms_code"]
},
"else": {
"not": { "required": ["sms_code"] }
}
}
)"_json;
// 2. 待校验的JSON数据(模拟合法请求)
const json valid_login_data = R"(
{
"username": "test_user123",
"password": "123456",
"login_type": "password",
"remember_me": true
}
)"_json;
// 待校验的JSON数据(模拟非法请求:sms类型缺少sms_code)
const json invalid_login_data = R"(
{
"username": "test_user",
"password": "123456",
"login_type": "sms",
"remember_me": false
}
)"_json;
try {
// 3. 初始化校验器,加载Schema
validator val;
val.set_root_schema(login_schema);
// 4. 校验合法数据
std::cout << "=== 校验合法数据 ===" << std::endl;
val.validate(valid_login_data);
std::cout << "校验通过!" << std::endl;
// 5. 校验非法数据
std::cout << "\n=== 校验非法数据 ===" << std::endl;
val.validate(invalid_login_data);
} catch (const nlohmann::json_schema::validation_error& e) {
// 捕获校验失败异常,输出错误信息
std::cerr << "校验失败:" << e.what() << std::endl;
std::cerr << "失败路径:" << e.instance_path() << std::endl;
std::cerr << "失败关键字:" << e.keyword() << std::endl;
} catch (const std::exception& e) {
// 其他异常(如Schema格式错误)
std::cerr << "异常:" << e.what() << std::endl;
}
return 0;
}
(3)运行结果
=== 校验合法数据 ===
校验通过!
=== 校验非法数据 ===
校验失败:Schema violation: required key 'sms_code' not present
失败路径:
失败关键字:required
4. 进阶实战:复用公共 Schema
在实际项目中,可将公共 Schema(如分页、时间戳)抽取为独立文件(如***mon_schema.json),通过文件读取加载,提升复用性。
(1)公共 Schema 文件 ***mon_schema.json
{
"$schema": "http://json-schema.org/draft/2020-12/schema",
"definitions": {
"page_param": {
"type": "object",
"properties": {
"page_num": { "type": "integer", "minimum": 1, "default": 1 },
"page_size": { "type": "integer", "minimum": 1, "maximum": 100, "default": 20 }
}
},
"timestamp": {
"type": "integer",
"minimum": 1000000000,
"description": "Unix时间戳(秒)"
}
}
}
(2)C++ 代码加载公共 Schema 并复用
#include <fstream>
#include <nlohmann/json.hpp>
#include <nlohmann/json-schema.hpp>
using json = nlohmann::json;
using validator = nlohmann::json_schema::validator;
// 读取JSON文件
json load_json_file(const std::string& path) {
std::ifstream file(path);
if (!file.is_open()) {
throw std::runtime_error("无法打开文件:" + path);
}
json data;
file >> data;
return data;
}
int main() {
try {
// 加载公共Schema
json ***mon_schema = load_json_file("***mon_schema.json");
// 定义业务Schema,引用公共分页参数
json query_schema = R"(
{
"$schema": "http://json-schema.org/draft/2020-12/schema",
"allOf": [
{ "$ref": "***mon_schema.json#/definitions/page_param" },
{
"type": "object",
"properties": {
"keyword": { "type": "string", "minLength": 1 },
"create_time": { "$ref": "***mon_schema.json#/definitions/timestamp" }
},
"required": ["keyword"]
}
]
}
)"_json;
// 初始化校验器,需要注册外部Schema的加载函数
validator val([](const std::string& uri) -> json {
// 处理外部Schema引用(这里uri是***mon_schema.json)
return load_json_file(uri);
});
val.set_root_schema(query_schema);
// 待校验数据
json query_data = R"(
{
"keyword": "test",
"page_num": 2,
"page_size": 10,
"create_time": 1699999999
}
)"_json;
// 执行校验
val.validate(query_data);
std::cout << "查询参数校验通过!" << std::endl;
} catch (const std::exception& e) {
std::cerr << "异常:" << e.what() << std::endl;
}
return 0;
}
6. 注意事项(C++ 开发避坑指南)
JSON 类型与 C++ 类型转换:
- JSON 的
integer可能对应 C++ 的int/long/uint64_t,校验时需确保数值范围不溢出(如minimum/maximum限制); - JSON 的
string是 UTF-8 编码,C++ 中处理中文时需注意编码一致性(避免长度计算错误)。
Schema 语法正确性:
- 2020-12 版本的
$schema关键字必须正确,否则校验器可能解析为旧版本; - 正则表达式中的反斜杠需双重转义(JSON 转义一次,C++ 字符串再转义一次,如
"pattern": "^1[3-9]\\\\d{9}$")。
性能考虑:
- 频繁校验时,建议缓存
validator实例(避免重复加载 Schema); - 对于超大型 JSON(如 10MB 以上),推荐使用
rapidjson方案(内存占用更低)。
错误处理:
- 捕获
validation_error异常时,通过instance_path()定位失败字段,便于调试; - 生产环境中,需将校验失败信息返回给前端(如接口错误码 + 失败字段说明)。
四、版本迁移指南(从旧版到 2020-12)
如果你的项目正在使用 Draft-07/2019-09,迁移到 2020-12 需注意以下变更:
| 旧版特性 | 2020-12 变更 | C++ 适配方式 |
|---|---|---|
$schema值为http://json-schema.org/draft-07/schema
|
改为http://json-schema.org/draft/2020-12/schema
|
直接修改 Schema 中的$schema字段 |
format关键字默认校验 |
2020-12 中format默认仅为注解,需开启断言 |
引入Format Assertion Vocabulary(见前文示例) |
additionalItems |
被unevaluatedItems替代(更灵活) |
若需禁止未定义数组元素,用"unevaluatedItems": false
|
exclusiveMinimum/exclusiveMaximum布尔值用法 |
仍支持,但推荐使用数值用法(如"exclusiveMinimum": 0) |
逐步迁移为数值用法,提升可读性 |
迁移步骤:
- 修改
$schema关键字为 2020-12 版本; - 替换已废弃的关键字(如
additionalItems→unevaluatedItems); - 开启需要的 vocabularies(如格式断言);
- 全面测试校验逻辑(确保无功能回归)。
五、总结
JSON Schema 2020-12 为 C++ 开发者提供了标准化、高效的数据校验方案,核心价值在于 “用 JSON 定义规则,用库实现校验”,彻底告别手写冗余校验代码。通过本文的学习,你已掌握:
- JSON Schema 的核心概念、语法规则和高级特性;
- C++ 生态中最优校验库的选型与环境搭建;
- 实战场景中的 Schema 定义、数据校验和公共规则复用;
- 版本迁移与避坑指南。
在实际项目中,建议将 JSON Schema 与接口文档(如 Swagger)结合使用,让前端、后端、测试人员基于同一套 “数据契约” 协作,大幅提升开发效率和接口稳定性。如果你需要处理更复杂的场景(如嵌套数组校验、动态字段约束),可以进一步研究Relative JSON Pointers和自定义校验关键字,JSON Schema 的灵活性足以支撑绝大多数业务需求。
后面可能会实现一个读取 Excel 文件(偏格式固定)到 Json 文件的小工具程序, 敬请期待!!!