cJSON 是一个用ANSI C开发的轻量级JSON处理库, 它的核心代码只有 cJSON.hcJSON.c 两个文件. 使用 cJSON 我们可以:

  • 将 JSON 字符串解析为 C 数据结构
  • 使用 C 数据结构构造 JSON 字符串

1 安装

https://github.com/DaveGamble/cJSON 取得最新代码后, 有两种方法”安装”它:

使用 cJSON 的源码需要包含它的头文件:

  1. #include <cjson/cJSON.h>

2 API介绍

这里以解析功能为主进行简单介绍, 详细内容请参考 cJSON GitHub.

2.1 数据结构

cJSON 的基本数据结构是 cJSON 结构体:

  1. /* The cJSON structure: */
  2. typedef struct cJSON
  3. {
  4. /* next/prev allow you to walk array/object chains.
  5. Alternatively, use GetArraySize/GetArrayItem/GetObjectItem */
  6. struct cJSON *next;
  7. struct cJSON *prev;
  8. /* An array or object item will have a child pointer
  9. pointing to a chain of the items in the array/object. */
  10. struct cJSON *child;
  11. /* The type of the item, as above. */
  12. int type;
  13. /* The item's string, if type==cJSON_String and type == cJSON_Raw */
  14. char *valuestring;
  15. /* writing to valueint is DEPRECATED,
  16. use cJSON_SetNumberValue instead */
  17. int valueint;
  18. /* The item's number, if type==cJSON_Number */
  19. double valuedouble;
  20. /* The item's name string, if this item is the child of,
  21. or is in the list of subitems of an object. */
  22. char *string;
  23. } cJSON;

其中 type 用来表示 cJSON 对象中存储的数据类型, 有以下数据类型

  • cJSON_Invalid 无效值
  • cJSON_True, cJSON_False 布尔值, 对应 JSON 中的 true , false
  • cJSON_NULL 空值, 对应 JSON 中的 null
  • cJSON_Number 数值, 对应的数值同时存放在 value_intvalue_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 为:

  1. {
  2. "name": "Awesome 4K",
  3. "resolutions": [
  4. {
  5. "width": 1280,
  6. "height": 720
  7. },
  8. {
  9. "width": 1920,
  10. "height": 1080
  11. },
  12. {
  13. "width": 3840,
  14. "height": 2160
  15. }
  16. ]
  17. }

JSON -> cJSON对象

我们通过 cJSON_Parse 函数可以直接把 JSON 字符串解析为 cJSON 对象. 如果由于 JSON 语法不对等原因造成解析失败, 可以通过 cJSON_GetErrorPtr 函数取得简单的错误信息, 它返回一个指向出错位置的字符指针.

  1. int ret = 0;
  2. cJSON* root = NULL;
  3. cJSON* name = NULL;
  4. cJSON* resolutions = NULL;
  5. const char* error_ptr;
  6. root = cJSON_Parse(s);
  7. if (NULL == root) {
  8. error_ptr = cJSON_GetErrorPtr();
  9. if (error_ptr != NULL) {
  10. fprintf(stderr, "Error before: %s\n", error_ptr);
  11. }
  12. ret = -1;
  13. goto end;
  14. }
  15. ...
  16. end:
  17. cJSON_Delete(root);
  18. return ret;

如果 JSON 文件有语法错误, error_ptr 会指出错误处的信息, 比如如果把 "name": "Awesome 4K", 中的逗号改为分号, 则出错信息为:

  1. $./parser demo.json
  2. Error before: ;
  3. "resolutions": [
  4. ...

访问cJSON对象

上一步已经把 JSON 字符串解析为 cJSON 对象, 现在可以对它们进行访问. 此示例解析到最内一层 name/value 时, 把它打印输出, 这种语法定义见 json.org:

  1. member
  2. ws string ws ':' element

首先输出 name, 再处理并输出 resolutions 数组中的每一项:

  1. name = cJSON_GetObjectItem(root, "name");
  2. if (NULL == name) {
  3. ret = -1;
  4. goto end;
  5. }
  6. print_object(name);
  7. resolutions = cJSON_GetObjectItem(root, "resolutions");
  8. if (NULL == resolutions || !cJSON_IsArray(resolutions)) {
  9. ret = -1;
  10. goto end;
  11. }
  12. ret = parse_resolutions(resolutions);

parse_resolutionsparse_resolution 函数:

  1. int parse_resolution(cJSON* resolution)
  2. {
  3. cJSON* node = NULL;
  4. node = cJSON_GetObjectItem(resolution, "width");
  5. if (NULL == node)
  6. return -1;
  7. print_object(node);
  8. node = cJSON_GetObjectItem(resolution, "height");
  9. if (NULL == node)
  10. return -1;
  11. print_object(node);
  12. return 0;
  13. }
  14. int parse_resolutions(cJSON* resolutions)
  15. {
  16. int ret;
  17. cJSON* node = NULL;
  18. printf("rules:\n");
  19. cJSON_ArrayForEach(node, resolutions)
  20. {
  21. ret = parse_resolution(node);
  22. if (ret < 0)
  23. return ret;
  24. printf("------\n");
  25. }
  26. return 0;
  27. }

简单的 cJSON 对象输出函数, 只输出特定对象:

  1. void print_object(const cJSON* obj)
  2. {
  3. if (cJSON_IsInvalid(obj)) {
  4. } else if (cJSON_IsObject(obj) || cJSON_IsArray(obj) || cJSON_IsRaw(obj)) {
  5. } else {
  6. printf("\"%s\": ", obj->string);
  7. if (cJSON_IsNull(obj)) {
  8. printf("null");
  9. } else if (cJSON_IsString(obj)) {
  10. printf("\"%s\"", obj->valuestring);
  11. } else if (cJSON_IsNumber(obj)) {
  12. printf("%d", obj->valueint);
  13. } else if (cJSON_IsBool(obj)) {
  14. printf("%s", cJSON_IsTrue(obj) ? "true" : "false");
  15. }
  16. printf("\n");
  17. }
  18. }

编译运行

  1. $make parser
  2. cc -Wall -g -O0 -o cjson/cJSON.o -c cjson/cJSON.c
  3. cc -Wall -g -O0 -o parser.o -c parser.c
  4. cc -Wall -g -O0 -I./cjson -o parser cjson/cJSON.o parser.o
  5. $./parser demo.json
  6. "name": "Awesome 4K"
  7. resolutions:
  8. "width": 1280
  9. "height": 720
  10. ------
  11. "width": 1920
  12. "height": 1080
  13. ------
  14. "width": 3840
  15. "height": 2160
  16. ------

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:

  1. typedef struct
  2. {
  3. const unsigned char *content;
  4. size_t length;
  5. size_t offset;
  6. size_t depth; /* How deeply nested (in arrays/objects)
  7. is the input at the current offset. */
  8. int lineno;
  9. internal_hooks hooks;
  10. } parse_buffer;

修改跳过空白字符的处理函数 buffer_skip_whitespace , 在遇到 \n 时累加行号:

  1. while (can_access_at_index(buffer, 0) &&
  2. (buffer_at_offset(buffer)[0] <= 32))
  3. {
  4. if(buffer_at_offset(buffer)[0] == '\n')
  5. buffer->lineno++;
  6. buffer->offset++;
  7. }

修改 error 结构体定义, 添加行号成员 lineo, 并修改全局变量的初始化代码:

  1. typedef struct {
  2. const unsigned char *json;
  3. size_t position;
  4. int lineno;
  5. } error;
  6. static error global_error = { NULL, 0, 0};

修改解析函数 cJSON_ParseWithOpts 中相关代码:

  1. // 初始化
  2. parse_buffer buffer = { 0, 0, 0, 0, 1, { 0, 0, 0 } };
  3. // 解析错误时, 把buffer的lineno赋值给error
  4. error local_error;
  5. ...
  6. local_error.lineno = buffer.lineno;

到此为止, 就可以在解析出错时把出错的行号添加到错误信息里.

接着添加一个新的错误信息获取函数 cJSON_GetErrorMsg , 它使用错误信息格式化调用者提供的字符缓冲区:

  1. CJSON_PUBLIC(const char *) cJSON_GetErrorMsg(char* err_buf)
  2. {
  3. if(err_buf == NULL)
  4. return NULL;
  5. snprintf(err_buf, CJSON_ERRBUF_SIZE, "line: %d, content: %s",
  6. global_error.lineno, (global_error.json + global_error.position));
  7. return err_buf;
  8. }

CJSON_ERRBUF_SIZE 宏定义和此函数的声明放在 cJSON.h :

  1. #define CJSON_ERRBUF_SIZE 128
  2. CJSON_PUBLIC(const char*) cJSON_GetErrorMsg(char* err_buf);

修改解析代码, 添加错误处理

修改 cJSON_Parse 的错误处理代码如下:

  1. root = cJSON_Parse(s);
  2. if (NULL == root) {
  3. fprintf(stderr, "parse error: %s\n", cJSON_GetErrorMsg(err_buf));
  4. ret = -1;
  5. goto end;
  6. }

编译运行

故意将 JSON 文件的第5行写错, 逗号写成分号:

  1. $cat -n demo2.json
  2. 1 {
  3. 2 "name": "Awesome 4K",
  4. 3 "resolutions": [
  5. 4 {
  6. 5 "width": 1280;
  7. 6 "height": 720
  8. 7 },

编译, 运行:

  1. $make parser2
  2. cc -Wall -g -O0 -o cjson2/cJSON.o -c cjson2/cJSON.c
  3. cc -Wall -g -O0 -o parser2.o -c parser2.c
  4. cc -Wall -g -O0 -I./cjson2 -o parser2 cjson2/cJSON.o parser2.o
  5. $./parser2 demo2.json
  6. parse error: line: 5, content: ;
  7. "height": 720
  8. },
  9. {
  10. "width": 1920,
  11. "height": 1080

可以看到已经指出第 5 行有错.

4.2 解析后的数据不满足业务逻辑时给出行号

实现思想

这比 JSON 语法错误时给出行号的处理要复杂, 思想是为每一个 cJSON 对象记录一个行号, 那么:

  • 如果 Object 中找不到指定的 name, 就给出 Object 对应的行号(相当于当前cJSON对象的父对象)
  • 如果 value 值不对, 就给出当前 cJSON 对象的行号

cJSON 在解析时遇到一个元素就会创建 cJSON 对象, 此时就是我们添加行号信息的好时机. 行号的取得已经在4.1节中描述了, 它保存在 parser_buffer 对象中.

修改cJSON

修改 cJSON.h . 定义无效行号, 因为行号从1开始, 所以0可以表示无效:

  1. #define CJSON_LINENO_INVALID 0

cJSON 结构体中添加行号成员:

  1. /* The cJSON structure: */
  2. typedef struct cJSON
  3. {
  4. ...
  5. int lineno;
  6. } cJSON;

修改 cJSON.c . 修改 cJSON 创建函数 cJSON_New_Item , 添加行号参数:

  1. /* Internal constructor. */
  2. static cJSON *cJSON_New_Item(const internal_hooks * const hooks,
  3. int lineno)
  4. {
  5. cJSON* node = (cJSON*)hooks->allocate(sizeof(cJSON));
  6. if (node)
  7. {
  8. memset(node, '\0', sizeof(cJSON));
  9. }
  10. node->lineno = lineno;
  11. return node;
  12. }

修改所有调用 cJSON_New_Item 函数的代码, 传入行号实参. 主要有两种调用, 解析 JSON 时传入 parser_buffer 的行号, 从 C 数据结构构造 JSON 字符串时传入 CJSON_LINENO_INVALID . 比如:

  • cJSON_ParseWithOpts 函数中, 创建根节点时

    1. item = cJSON_New_Item(&global_hooks, buffer.lineno);
  • parse_array

    1. /* allocate next item */
    2. cJSON *new_item = cJSON_New_Item(&(input_buffer->hooks),
    3. input_buffer->lineno);
  • create_reference 函数, 传入无效行号

    1. reference = cJSON_New_Item(hooks, CJSON_LINENO_INVALID);

一共有 18 处, 都需要修改.

修改解析代码, 添加错误处理

添加错误处理信息获取函数 parse_get_error:

  1. static char g_errbuf[CJSON_ERRBUF_SIZE];
  2. const char* parse_get_error(const cJSON* obj)
  3. {
  4. if (cJSON_IsObject(obj) && obj->prev == NULL && obj->next == NULL) {
  5. snprintf(g_errbuf, CJSON_ERRBUF_SIZE, "object: <root>, line: %d",
  6. obj->lineno);
  7. } else {
  8. snprintf(g_errbuf, CJSON_ERRBUF_SIZE, "object: %s, line: %d",
  9. obj->string, obj->lineno);
  10. }
  11. return g_errbuf;
  12. }

为简单, 我们只检查两处业务逻辑, 一个是检查”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

参考

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