JSON处理C函数库-cJSON
2024-10-28 14:26:43 阿炯

libcjson是一个用于 JSON 数据编解码的轻量级C函数库,文件只有 500 多行,速度理想。其代码维护良好,结构简单易懂,对于想要学习 C 语言项目或者处理简单 JSON 数据的开发者来说是一个很好的选择。虽然功能不是极其强大,但在很多对性能要求较高且 JSON 操作不特别复杂的场景下非常适用。采用MIT协议授权。

libcjson is an ultralightweight JSON parser in ANSI C. cJSON is written in ANSI C (or C89, C90). If your compiler or C library doesn't follow this standard, correct behavior is not guaranteed.

适用于小型项目或者对资源占用要求严格的系统中,用于 JSON 数据的生成和解析,比如在嵌入式系统中与其他设备进行简单的 JSON 格式数据通信,或者在一些轻量级的应用程序中处理配置文件等。使用uJson库有两种方式:使用编译好的动态库或者静态库;将源码导入到自己工程中一起参与编译,cJson.c和cJson.h。

核心功能:
特性说明
超轻量整个库就一个 cJSON.c 和一个 cJSON.h,复制粘贴就能用
高兼容使用 ANSI C (C89) 标准编写,几乎所有平台都能运行
简单直接API 设计直观,专注于解析和生成两件事


基本类型

null is created with cJSON_CreateNull,

booleans are created with cJSON_CreateTrue, cJSON_CreateFalse or cJSON_CreateBool,

numbers are created with cJSON_CreateNumber. This will set both valuedouble and valueint. If the number is outside of the range of an integer, INT_MAX or INT_MIN are used for valueint,

strings are created with cJSON_CreateString (copies the string) or with cJSON_CreateStringReference (directly points to the string. This means that valuestring won't be deleted by cJSON_Delete and you are responsible for its lifetime, useful for constants).

另外有Arrays、Objects类型。

由于C语言是强类型语言,任何json中的独立属性对于C实现来说都视为一个独立的对象,然后用指针将这些对象联系起来;具体的话,可以直接阅读cJson.h的函数,都很直观。使用时一定要注意动态内存的释放!尽管json更现代,但是在实际嵌入式平台,如果没有系统内存管理支撑,不建议用c去处理json数据,动态内存分配着实有点令人担心。

克隆代码仓库后在项目文件夹下新建build文件夹,注意这里的编译器要与最终你项目的编译器统一;在build文件夹下执行Cmake生成makefile文件。
cJSON\build>cmake .. -G "Unix Makefiles" -DENABLE_CJSON_TEST=off

也可执行make编译,生成cJson动态库。在build文件夹下生成dll动态库,动态库的特点就是使用的时候不链接的,运行的程序的时候再调用。所以这个dll文件需要放到跟可执行程序相同目录下。其实这个库只有一个cJson.c文件,可以在自己项目中一起编译。

集成步骤
第1步:从 GitHub 下载源码
第2步:把 cJSON.h 和 cJSON.c 扔进你的项目目录
第3步:在代码里加一行
#include "cJSON.h"

如果把文件放在其它目录里,比如 /freeoa/libs/cjson/,那就改成:
#include "/freeoa/libs/cjson/cJSON.h"

在 cJSON 的世界里,不管是对象、数组,还是一个简单的数字,都用同一个结构体 cJSON 来表示。C 语言处理JSON,最怕的就是内存泄漏。

常用的类型与函数对应
想创建的对象 对应的函数
对象 {} cJSON_CreateObject()
数组 [] cJSON_CreateArray()
字符串 "hello" cJSON_CreateString("hello")
数字 42 cJSON_CreateNumber(42)
布尔值 cJSON_CreateTrue() / cJSON_CreateFalse()
空值 null cJSON_CreateNull()

假设要生成这样一段 JSON:
{
    "name": "Awesome Scren",
    "resolutions": [
        {"width": 1280, "height": 720},
        {"width": 1920, "height": 1080},
        {"width": 3840, "height": 2160}
    ]
}

代码如下:
#include <stdio.h>
#include <stdlib.h>
#include "cJSON.h"

char* create_monitor_json(void) {
    // 1. 创建根对象
    cJSON *monitor = cJSON_CreateObject();

    // 2. 添加名称字段
    cJSON_AddStringToObject(monitor, "name", "Awesome Scren");

    // 3. 创建分辨率数组
    cJSON *resolutions = cJSON_AddArrayToObject(monitor, "resolutions");

    // 4. 往数组里塞数据
    int res_data[3][2] = {{1280, 720}, {1920, 1080}, {3840, 2160}};

    for (int i = 0; i < 3; i++) {
        cJSON *res = cJSON_CreateObject();
        cJSON_AddNumberToObject(res, "width", res_data[i][0]);
        cJSON_AddNumberToObject(res, "height", res_data[i][1]);
        cJSON_AddItemToArray(resolutions, res);
    }

    // 5. 转成字符串
    char *json_str = cJSON_Print(monitor);

    // 6. 释放 cJSON 对象
    cJSON_Delete(monitor);

    return json_str;  // 调用者需要 free 这个字符串
}

int main(void) {
    char *result = create_monitor_json();
    printf("%s\n", result);
    free(result);  // 别忘了释放!
    return 0;
}

构建流程图解


打印函数
• cJSON_Print():生成带缩进的格式化字符串,适合调试和阅读;
• cJSON_PrintUnformatted():生成紧凑字符串,适合网络传输。

注意:cJSON_Print() 返回的字符串是在堆上分配的,用完必须 free() 进行释放,否则内存泄漏。

解析外部的 JSON 字符串

上面演示创建 JSON,那收到一串 JSON 字符串怎么读取呢?

核心函数:cJSON_Parse
cJSON *root = cJSON_Parse(json_string);

这个函数会把字符串解析成一棵 cJSON 对象树。

但有一个关键点:一定要检查返回值是不是 NULL(这也是C中常见的基本操作了)。

cJSON *root = cJSON_Parse(json_string);
if (root == NULL) {
    // 解析失败,可能是 JSON 格式不对
    const char *error = cJSON_GetErrorPtr();
    printf("解析错误:%s\n", error);
    return;
}

不检查直接用,程序会直接崩溃。完整的示例参考下文的示例3。

提取数据

1:安全访问对象成员

cJSON *name = cJSON_GetObjectItemCaseSensitive(root, "name");
这个函数有个很棒的特性:自带空指针检查。即使 root 是 NULL,它也不会崩溃,而是返回 NULL。

这意味着你可以安全地链式调用:
// 即使中间某一层是 NULL 也不会崩
cJSON *city = cJSON_GetObjectItemCaseSensitive(
    cJSON_GetObjectItemCaseSensitive(root, "address"),
    "city"
);

2:类型检查

拿到数据后,别急着用,先问一句"是什么类型":
if (cJSON_IsString(name) && name->valuestring != NULL) {
    printf("名称:%s\n", name->valuestring);
}

if (cJSON_IsNumber(price)) {
    printf("价格:%.2f\n", price->valuedouble);
}
这是防御性编程的好习惯。万一服务器返回的数据结构和你预期的不一样,至少程序不会直接挂掉。

3:高效遍历数组

cJSON *item = NULL;
cJSON_ArrayForEach(item, resolutions) {
    cJSON *width = cJSON_GetObjectItemCaseSensitive(item, "width");
    cJSON *height = cJSON_GetObjectItemCaseSensitive(item, "height");
    // 处理每个分辨率...
}

cJSON_ArrayForEach 是一个宏,用起来就像 for 循环一样自然。但有两条原则必须要遵守(也是C编程的基本操作):

原则1:谁创建,谁释放
不管是用 cJSON_Parse() 解析出来的,还是用 cJSON_CreateObject() 创建的,只要是顶层对象,用完必须调用 cJSON_Delete()。

// 解析场景
cJSON *root = cJSON_Parse(json_str);
// ... 使用 root ...
cJSON_Delete(root);  // 必须释放

// 创建场景
cJSON *obj = cJSON_CreateObject();
// ... 使用 obj ...
cJSON_Delete(obj);  // 必须释放
cJSON_Delete() 会递归释放整棵树,包括所有子节点。

原则2:所有权转移
当把一个节点用 cJSON_AddItemToObject() 或 cJSON_AddItemToArray() 添加到另一个对象里时,它的"主动权"就是别人的了。

正确做法:
cJSON *root = cJSON_CreateObject();
cJSON *child = cJSON_CreateString("hello");

cJSON_AddItemToObject(root, "greeting", child);
// child 的所有权已经转移给 root 了

// ... 使用 root ...

cJSON_Delete(root);  // 只需要释放 root,child 会被自动释放

错误做法(程序会崩!):
cJSON *root = cJSON_CreateObject();
cJSON *child = cJSON_CreateString("hello");

cJSON_AddItemToObject(root, "greeting", child);

//错误!child 已经属于 root 了
cJSON_Delete(child);  // 这里释放了一次

//错误!root 删除时会再释放 child
cJSON_Delete(root);   // 重复释放,程序崩溃!
这叫重复释放(double free),是 C 语言内存管理中最经典的坑之一。

也即:

正确操作
1.Parse/Create的顶层对象>必须Delete;
2.子节点 Add 到父节点后>只 Delete 父节点;
3.CJSON Print的返回值→必须 free。

错误操作
1.Add 之后又 Delete 子节点 → 重复释放,崩溃;
2.忘记 Delete 顶层对象 → 内存泄漏;
3.忘记 free 打印的字符串 → 内存泄漏。

核心 API 速查
功能 函数
解析 JSON 字符串 cJSON_Parse()
创建对象/数组 cJSON_CreateObject() / cJSON_CreateArray()
添加成员 cJSON_AddStringToObject() / cJSON_AddNumberToObject()
获取成员 cJSON_GetObjectItemCaseSensitive()
遍历数组 cJSON_ArrayForEach()
类型检查 cJSON_IsString() / cJSON_IsNumber() / ...
转成字符串 cJSON_Print() / cJSON_PrintUnformatted()
释放内存 cJSON_Delete()


编码示例1

#include <stdio.h>
#include "cJSON.h"

int main() {
    // 创建一个JSON对象
    cJSON *root = cJSON_CreateObject();
    if (root == NULL) {
        return 1;
    }

    // 在JSON对象中添加一个字符串类型的键值对
    cJSON_AddItemToObject(root, "name", cJSON_CreateString("John"));

    // 将JSON对象转换为字符串
    char *json_string = cJSON_Print(root);
    if (json_string == NULL) {
        cJSON_Delete(root);
        return 1;
    }

    // 打印JSON字符串
    printf("%s\n", json_string);

    // 释放内存
    cJSON_Delete(root);
    free(json_string);

    return 0;
}

解码示例

#include <stdio.h>
#include "cJSON.h"

int main() {
    const char *json_string = "{\"name\":\"John\"}";

    // 解析JSON字符串
    cJSON *root = cJSON_Parse(json_string);
    if (root == NULL) {
        const char *error_ptr = cJSON_GetErrorPtr();
        if (error_ptr!= NULL) {
            printf("Error before: %s\n", error_ptr);
        }
        return 1;
    }

    // 获取"name"键对应的值
    cJSON *name_json = cJSON_GetObjectItem(root, "name");
    if (cJSON_IsString(name_json)) {
        printf("Name: %s\n", name_json->valuestring);
    }

    // 释放内存
    cJSON_Delete(root);

    return 0;
}

编码示例2

封装Json结构并打印我们假定一个json数据结构体:{
    "APP_IS_VALID": true,
    "REQUEST_UPDATE": false,
    "APP_INFO": {
        "VERSION": "0.00.001",
        "RELEASE_DATE": "2025-05-16",
        "SH256": "0123456789abcdef"
    },
    "CONFIGURATION_TYPE_LIST": ["High","Normal","Low"]
}

build中存放编译相关文件,记得拷贝cJson.dll到build文件夹,在main.c中实现测试。

#include <stdio.h>
#include <stddef.h>
#include "cJson/cJson.h"
#include <stdlib.h>

void demo1(void) {
    cJSON *APP_INFO = cJSON_CreateObject();
    cJSON_AddItemToObject(APP_INFO, "VERSION", cJSON_CreateString("0.00.001"));
    cJSON_AddItemToObject(APP_INFO, "RELEASE_DATE", cJSON_CreateString("2025-05-16"));
    cJSON_AddItemToObject(APP_INFO, "SH256", cJSON_CreateString("0123456789abcdef"));

    cJSON* CONFIGURATION_TYPE_LIST = cJSON_CreateArray();
    cJSON_AddItemToArray(CONFIGURATION_TYPE_LIST, cJSON_CreateString("High"));
    cJSON_AddItemToArray(CONFIGURATION_TYPE_LIST, cJSON_CreateString("Normal"));
    cJSON_AddItemToArray(CONFIGURATION_TYPE_LIST, cJSON_CreateString("Low"));

    cJSON* GLOBAL_CFG = cJSON_CreateObject();
    cJSON_AddItemToObject(GLOBAL_CFG, "APP_IS_VALID", cJSON_CreateBool(0));
    cJSON_AddItemToObject(GLOBAL_CFG, "REQUEST_UPDATE", cJSON_CreateBool(0));
    cJSON_AddItemToObject(GLOBAL_CFG, "APP_INFO", APP_INFO);
    cJSON_AddItemToObject(GLOBAL_CFG, "CONFIGURATION_TYPE_LIST", CONFIGURATION_TYPE_LIST);

    char *cPrint = cJSON_Print(GLOBAL_CFG);
    char *cPrintUnformatted = cJSON_PrintUnformatted(GLOBAL_CFG);
   

    printf("cJSON_Print:\n%s\n", cPrint);
    printf("cJSON_PrintUnformatted:\n%s\n", cPrintUnformatted);
   
    // 记得使用cJSON_Print 和 cJSON_PrintUnformatted返回来的字符指针需要free掉内存!
    free(cPrint);
    free(cPrintUnformatted);
}


int main(void) {
    demo1();
}

编译通过后直接执行可以看到结果。

解析Json文件并获取属性

解析时从严谨和安全角度,需要使用结构体中的type类型进行判断,再取数据或者对象;可以使用cJSON_Print函数去获取字符串,也可以使用结构体中的valuestring取值,但要注意的是,使用cJSON_Print函数去获取字符串之后需要free掉获取到的指针,否则会造成内存泄漏。
#include <string.h>
#include <sys/stat.h>
#include <stdbool.h>

void demo2() {
    /* open json file*/
    FILE *file = NULL;
    file = fopen("../cfg.json", "r");
    if (file == NULL) {
        printf("Open file failed!\n");
        return;
    }
    /* fetch file's size */
    struct stat statBuffer;
    stat("../cfg.json", &statBuffer);
    int fileSize = statBuffer.st_size;

    /* malloc memory*/
    char *jsonStr = (char *)malloc(sizeof(char) * fileSize + 1);
    memset(jsonStr, 0, fileSize + 1);

    /* read string */
    int size = fread(jsonStr, sizeof(char), fileSize, file);
    if (size == 0) {
        printf("Read file Failed!\n");
        fclose(file);
        return;
    }
    printf("%s\n", jsonStr);
    fclose(file);

    /* parser string to jsonString*/
    cJSON *root = cJSON_Parse(jsonStr);
    if (!root) {
        printf("Error before: [%s]\n", cJSON_GetErrorPtr());
        free(jsonStr);
        return;
    }
    free(jsonStr);


    cJSON *item = NULL;
    char *v_str = NULL;
    double v_double = 0.0;
    int v_int = 0;
    bool v_bool = false;

    /* 获取APP_INFO对象 */
    item = cJSON_GetObjectItem(root, "APP_INFO");
    if (item != NULL) {
        /* 判断是否是新的json对象 */
        if (item->type == cJSON_Object) {
            cJSON* version = NULL;
            /* 获取VERSION对象 */
            version = cJSON_GetObjectItem(item, "VERSION");
            if (version->type == cJSON_String) {
                v_str = version->valuestring;
                printf("VERSION = %s\n", v_str);
            }
        }
    }
}


int main(void) {
    demo2();
}

在上面的示例中,先从cfg.json文件中一次性读取所有的字符信息,然后将字符一次性转换成jsonString然后解析。这里以获取APP的版本号为例。

编码示例3(检查显示器是否支持 1080P)

#include <stdio.h>
#include "cJSON.h"

// 返回 1 表示支持,0 表示不支持
int supports_full_hd(const char *json_str) {
    cJSON *root = cJSON_Parse(json_str);

    // 解析失败检查
    if (root == NULL) {
        printf("JSON 解析失败\n");
        return 0;
    }

    // 获取显示器名称(可选)
    cJSON *name = cJSON_GetObjectItemCaseSensitive(root, "name");
    if (cJSON_IsString(name)) {
        printf("正在检查显示器:%s\n", name->valuestring);
    }

    // 遍历分辨率数组
    cJSON *resolutions = cJSON_GetObjectItemCaseSensitive(root, "resolutions");
    cJSON *res = NULL;

    cJSON_ArrayForEach(res, resolutions) {
        cJSON *w = cJSON_GetObjectItemCaseSensitive(res, "width");
        cJSON *h = cJSON_GetObjectItemCaseSensitive(res, "height");

        if (cJSON_IsNumber(w) && cJSON_IsNumber(h)) {
            if (w->valuedouble == 1920 && h->valuedouble == 1080) {
                cJSON_Delete(root);  // 别忘了释放
                return 1;  // 找到了!
            }
        }
    }
    cJSON_Delete(root);  // 别忘了释放
    return 0;
}


最新版本:1.7
v1.7.8于2024年5月发布。

项目主页:https://github.com/DaveGamble/cJSON