cJSON 是一个用ANSI C开发的轻量级JSON处理库, 它的核心代码只有 cJSON.h 和 cJSON.c 两个文件. 使用 cJSON 我们可以:
- 将 JSON 字符串解析为 C 数据结构
- 使用 C 数据结构构造 JSON 字符串
1 安装
从 https://github.com/DaveGamble/cJSON 取得最新代码后, 有两种方法”安装”它:
- 直接把
cJSON.h和cJSON.c文件拷贝到我们自己的源码目录, 与其他源码一起编译 - 使用 CMake 编译安装, 参考 https://github.com/DaveGamble/cJSON
使用 cJSON 的源码需要包含它的头文件:
#include <cjson/cJSON.h>
2 API介绍
这里以解析功能为主进行简单介绍, 详细内容请参考 cJSON GitHub.
2.1 数据结构
cJSON 的基本数据结构是 cJSON 结构体:
/* The cJSON structure: */typedef struct cJSON{/* next/prev allow you to walk array/object chains.Alternatively, use GetArraySize/GetArrayItem/GetObjectItem */struct cJSON *next;struct cJSON *prev;/* An array or object item will have a child pointerpointing to a chain of the items in the array/object. */struct cJSON *child;/* The type of the item, as above. */int type;/* The item's string, if type==cJSON_String and type == cJSON_Raw */char *valuestring;/* writing to valueint is DEPRECATED,use cJSON_SetNumberValue instead */int valueint;/* The item's number, if type==cJSON_Number */double valuedouble;/* The item's name string, if this item is the child of,or is in the list of subitems of an object. */char *string;} cJSON;
其中 type 用来表示 cJSON 对象中存储的数据类型, 有以下数据类型
- cJSON_Invalid 无效值
- cJSON_True, cJSON_False 布尔值, 对应 JSON 中的
true,false - cJSON_NULL 空值, 对应 JSON 中的
null - cJSON_Number 数值, 对应的数值同时存放在
value_int和value_double中 - cJSON_String 字符串值, 表示以0结尾的字符串, 值存放在
value_string中 - cJSON_Array 数组, 包含一个或多个cJSON对象, 它们通过
child指向的链表表示, 并通过prev,next连接在一起. 数组第一项prev == NULL, 最后一项next == NULL, 所以它并不是一个双向循环链表 - cJSON_Object 对象, 内部存储方式类似 cJSON_Array, 但对象的各个成员的名字存储在各自的
string中 - cJSON_Raw raw值, 不用于解析, 具体见官方文档
:::info
type 并不是一个枚举式的值, 而是一个bit-flag值, 因此不能通过比较或 switch/case 方式来使用它, 而应该使用 cJSON_IsXXX() 函数来取得 cJSON 存储的数据类型.
:::
2.2 解析JSON
解析 JSON 的 API:
cJSON_Parse解析 JSON 字符串cJSON_GetObjectItem取得对象的成员cJSON_ArrayForEach遍历数组- 通过
cJSON_IsXXX判断 cJSON 对象类型, 然后通过value_int,value_double,value_string等进行访问2.3 构造JSON
// TODO
构造好之后通过cJSON_Print来将 cJSON 对象树转换为 JSON 字符串
3 基本使用
以下通过2个简单示例来演示 cJSON API的基本使用, 所用的 JSON 字符串和代码来自官方github, 略有差别.
本文用到的所有代码见 我的GitHub.
3.1 解析JSON
要解析的 JSON 为:
{"name": "Awesome 4K","resolutions": [{"width": 1280,"height": 720},{"width": 1920,"height": 1080},{"width": 3840,"height": 2160}]}
JSON -> cJSON对象
我们通过 cJSON_Parse 函数可以直接把 JSON 字符串解析为 cJSON 对象. 如果由于 JSON 语法不对等原因造成解析失败, 可以通过 cJSON_GetErrorPtr 函数取得简单的错误信息, 它返回一个指向出错位置的字符指针.
int ret = 0;cJSON* root = NULL;cJSON* name = NULL;cJSON* resolutions = NULL;const char* error_ptr;root = cJSON_Parse(s);if (NULL == root) {error_ptr = cJSON_GetErrorPtr();if (error_ptr != NULL) {fprintf(stderr, "Error before: %s\n", error_ptr);}ret = -1;goto end;}...end:cJSON_Delete(root);return ret;
如果 JSON 文件有语法错误, error_ptr 会指出错误处的信息, 比如如果把 "name": "Awesome 4K", 中的逗号改为分号, 则出错信息为:
$./parser demo.jsonError before: ;"resolutions": [...
访问cJSON对象
上一步已经把 JSON 字符串解析为 cJSON 对象, 现在可以对它们进行访问. 此示例解析到最内一层 name/value 时, 把它打印输出, 这种语法定义见 json.org:
memberws string ws ':' element
首先输出 name, 再处理并输出 resolutions 数组中的每一项:
name = cJSON_GetObjectItem(root, "name");if (NULL == name) {ret = -1;goto end;}print_object(name);resolutions = cJSON_GetObjectItem(root, "resolutions");if (NULL == resolutions || !cJSON_IsArray(resolutions)) {ret = -1;goto end;}ret = parse_resolutions(resolutions);
parse_resolutions 与 parse_resolution 函数:
int parse_resolution(cJSON* resolution){cJSON* node = NULL;node = cJSON_GetObjectItem(resolution, "width");if (NULL == node)return -1;print_object(node);node = cJSON_GetObjectItem(resolution, "height");if (NULL == node)return -1;print_object(node);return 0;}int parse_resolutions(cJSON* resolutions){int ret;cJSON* node = NULL;printf("rules:\n");cJSON_ArrayForEach(node, resolutions){ret = parse_resolution(node);if (ret < 0)return ret;printf("------\n");}return 0;}
简单的 cJSON 对象输出函数, 只输出特定对象:
void print_object(const cJSON* obj){if (cJSON_IsInvalid(obj)) {} else if (cJSON_IsObject(obj) || cJSON_IsArray(obj) || cJSON_IsRaw(obj)) {} else {printf("\"%s\": ", obj->string);if (cJSON_IsNull(obj)) {printf("null");} else if (cJSON_IsString(obj)) {printf("\"%s\"", obj->valuestring);} else if (cJSON_IsNumber(obj)) {printf("%d", obj->valueint);} else if (cJSON_IsBool(obj)) {printf("%s", cJSON_IsTrue(obj) ? "true" : "false");}printf("\n");}}
编译运行
$make parsercc -Wall -g -O0 -o cjson/cJSON.o -c cjson/cJSON.ccc -Wall -g -O0 -o parser.o -c parser.ccc -Wall -g -O0 -I./cjson -o parser cjson/cJSON.o parser.o$./parser demo.json"name": "Awesome 4K"resolutions:"width": 1280"height": 720------"width": 1920"height": 1080------"width": 3840"height": 2160------
3.2 使用C数据结构构造JSON
// TODO
4 对出错信息的改进
从3.1节的例子可以看到 , 在JSON 解析出错时, 可以调用 cJSON_GetErrorPtr 来获取出错位置处的字符串, 但我觉得这个出错信息过于简单. 我还需要以下功能:
- JSON 语法错误导致
cJSON_Parse失败时给出出错的行号 - 解析出的 cJSON 对象不满足我们的业务逻辑时给出出错的行号
比如,resolutions写成resolution, 或者width的值不是数值类型, 或者数值类型超时范围
接下来通过对 cJSON 进行改进来添加这两个功能
4.1 JSON语法错误时给出行号
修改cJSON
首先修改 cJSON.c . cJSON 在解析 JSON 时使用 parser_buffer 结构体来保存相关信息, 给这个结构体添加当前行号成员 lineno:
typedef struct{const unsigned char *content;size_t length;size_t offset;size_t depth; /* How deeply nested (in arrays/objects)is the input at the current offset. */int lineno;internal_hooks hooks;} parse_buffer;
修改跳过空白字符的处理函数 buffer_skip_whitespace , 在遇到 \n 时累加行号:
while (can_access_at_index(buffer, 0) &&(buffer_at_offset(buffer)[0] <= 32)){if(buffer_at_offset(buffer)[0] == '\n')buffer->lineno++;buffer->offset++;}
修改 error 结构体定义, 添加行号成员 lineo, 并修改全局变量的初始化代码:
typedef struct {const unsigned char *json;size_t position;int lineno;} error;static error global_error = { NULL, 0, 0};
修改解析函数 cJSON_ParseWithOpts 中相关代码:
// 初始化parse_buffer buffer = { 0, 0, 0, 0, 1, { 0, 0, 0 } };// 解析错误时, 把buffer的lineno赋值给errorerror local_error;...local_error.lineno = buffer.lineno;
到此为止, 就可以在解析出错时把出错的行号添加到错误信息里.
接着添加一个新的错误信息获取函数 cJSON_GetErrorMsg , 它使用错误信息格式化调用者提供的字符缓冲区:
CJSON_PUBLIC(const char *) cJSON_GetErrorMsg(char* err_buf){if(err_buf == NULL)return NULL;snprintf(err_buf, CJSON_ERRBUF_SIZE, "line: %d, content: %s",global_error.lineno, (global_error.json + global_error.position));return err_buf;}
CJSON_ERRBUF_SIZE 宏定义和此函数的声明放在 cJSON.h :
#define CJSON_ERRBUF_SIZE 128CJSON_PUBLIC(const char*) cJSON_GetErrorMsg(char* err_buf);
修改解析代码, 添加错误处理
修改 cJSON_Parse 的错误处理代码如下:
root = cJSON_Parse(s);if (NULL == root) {fprintf(stderr, "parse error: %s\n", cJSON_GetErrorMsg(err_buf));ret = -1;goto end;}
编译运行
故意将 JSON 文件的第5行写错, 逗号写成分号:
$cat -n demo2.json1 {2 "name": "Awesome 4K",3 "resolutions": [4 {5 "width": 1280;6 "height": 7207 },
编译, 运行:
$make parser2cc -Wall -g -O0 -o cjson2/cJSON.o -c cjson2/cJSON.ccc -Wall -g -O0 -o parser2.o -c parser2.ccc -Wall -g -O0 -I./cjson2 -o parser2 cjson2/cJSON.o parser2.o$./parser2 demo2.jsonparse error: line: 5, content: ;"height": 720},{"width": 1920,"height": 1080
可以看到已经指出第 5 行有错.
4.2 解析后的数据不满足业务逻辑时给出行号
实现思想
这比 JSON 语法错误时给出行号的处理要复杂, 思想是为每一个 cJSON 对象记录一个行号, 那么:
- 如果 Object 中找不到指定的 name, 就给出 Object 对应的行号(相当于当前cJSON对象的父对象)
- 如果 value 值不对, 就给出当前 cJSON 对象的行号
cJSON 在解析时遇到一个元素就会创建 cJSON 对象, 此时就是我们添加行号信息的好时机. 行号的取得已经在4.1节中描述了, 它保存在 parser_buffer 对象中.
修改cJSON
修改 cJSON.h . 定义无效行号, 因为行号从1开始, 所以0可以表示无效:
#define CJSON_LINENO_INVALID 0
cJSON 结构体中添加行号成员:
/* The cJSON structure: */typedef struct cJSON{...int lineno;} cJSON;
修改 cJSON.c . 修改 cJSON 创建函数 cJSON_New_Item , 添加行号参数:
/* Internal constructor. */static cJSON *cJSON_New_Item(const internal_hooks * const hooks,int lineno){cJSON* node = (cJSON*)hooks->allocate(sizeof(cJSON));if (node){memset(node, '\0', sizeof(cJSON));}node->lineno = lineno;return node;}
修改所有调用 cJSON_New_Item 函数的代码, 传入行号实参. 主要有两种调用, 解析 JSON 时传入 parser_buffer 的行号, 从 C 数据结构构造 JSON 字符串时传入 CJSON_LINENO_INVALID . 比如:
cJSON_ParseWithOpts函数中, 创建根节点时item = cJSON_New_Item(&global_hooks, buffer.lineno);
parse_array时/* allocate next item */cJSON *new_item = cJSON_New_Item(&(input_buffer->hooks),input_buffer->lineno);
create_reference函数, 传入无效行号reference = cJSON_New_Item(hooks, CJSON_LINENO_INVALID);
修改解析代码, 添加错误处理
添加错误处理信息获取函数 parse_get_error:
static char g_errbuf[CJSON_ERRBUF_SIZE];const char* parse_get_error(const cJSON* obj){if (cJSON_IsObject(obj) && obj->prev == NULL && obj->next == NULL) {snprintf(g_errbuf, CJSON_ERRBUF_SIZE, "object: <root>, line: %d",obj->lineno);} else {snprintf(g_errbuf, CJSON_ERRBUF_SIZE, "object: %s, line: %d",obj->string, obj->lineno);}return g_errbuf;}
为简单, 我们只检查两处业务逻辑, 一个是检查”name”是否出现在 JSON 中:
name = cJSON_GetObjectItem(root, "name");
if (NULL == name) {
fprintf(stderr, "can't find \"name\": %s\n", parse_get_error(root));
ret = -1;
goto end;
}
另一处检查”resolutions”数组中的”width”的值是不是数值:
node = cJSON_GetObjectItem(resolution, "width");
if (NULL == node)
return -1;
if (!cJSON_IsNumber(node)) {
fprintf(stderr, "invalid \"width\" value: %s\n", parse_get_error(node));
return -1;
}
编译运行
编译:
$make parser2
cc -Wall -g -O0 -o cjson2/cJSON.o -c cjson2/cJSON.c
cc -Wall -g -O0 -o parser2.o -c parser2.c
cc -Wall -g -O0 -I./cjson2 -o parser2 cjson2/cJSON.o parser2.o
构造错误JSON, 运行. 错误1:
$cat -n demo3.json
1 {
2 "nam": "Awesome 4K", # name写错成nam
3 "resolutions": [
4 {
$./parser2 demo3.json
can't find "name": object: <root>, line: 1
错误2:
$cat -n demo4.json
1 {
2 "name": "Awesome 4K",
3 "resolutions": [
4 {
5 "width": "what?", # width的值写错成字符串
6 "height": 720
7 },
$./parser2 ./demo4.json
"name": "Awesome 4K"
resolutions:
invalid "width" value: object: width, line: 5
5 代码说明
代码见 https://github.com/zzqcn/storage/tree/master/code/c/cjson
目录结构:
.
├── cjson/ // cJSON 1.7.12原版
├── cjson2/ // cJSON 1.7.12出错信息改进版
├── demo.json // JSON样本
├── builder.c // 从C数据结构构造JSON
├── parser.c // 使用原版cJSON解析JSON
└── parser2.c // 使用改进版cJSON解析JSON, 给出更多错误信息
改进版的cJSON: https://github.com/zzqcn/cJSON/tree/error-line
参考

本作品采用知识共享署名-非商业性使用-禁止演绎 3.0 未本地化版本许可协议进行许可。
