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 pointer
pointing 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.json
Error before: ;
"resolutions": [
...
访问cJSON对象
上一步已经把 JSON 字符串解析为 cJSON 对象, 现在可以对它们进行访问. 此示例解析到最内一层 name/value
时, 把它打印输出, 这种语法定义见 json.org:
member
ws 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 parser
cc -Wall -g -O0 -o cjson/cJSON.o -c cjson/cJSON.c
cc -Wall -g -O0 -o parser.o -c parser.c
cc -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赋值给error
error 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 128
CJSON_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.json
1 {
2 "name": "Awesome 4K",
3 "resolutions": [
4 {
5 "width": 1280;
6 "height": 720
7 },
编译, 运行:
$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
$./parser2 demo2.json
parse 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 未本地化版本许可协议进行许可。