概要
cJSON 是一个用 ANSI C 编写的轻量级 JSON 解析/生成库,MIT 许可,核心通常只有 cJSON.c 和 cJSON.h 两个文件。它非常适合嵌入式、C 项目配置文件、中小型 JSON 数据的解析和生成。cJSON 会把 JSON 文本解析成一棵内存中的节点树,然后通过 API 遍历和读取。
下面按"解析 JSON 文件"这个使用场景全面介绍。
1. cJSON 特点与局限
特点:
- 纯 C,ANSI C,可移植性好。
- 单文件,容易集成到工程。
- 支持标准 JSON:对象、数组、字符串、数字、布尔、null。
- 支持解析、生成、修改、打印、压缩。
- API 简单,命名统一以
cJSON_开头。 - MIT 许可证,商用友好。
局限:
- 数字内部用
double存储,大整数可能精度丢失。 - 不支持 JSON 注释、单引号、尾随逗号等非标准语法。
- 不支持流式解析,需要一次性把 JSON 读入内存。
cJSON_GetErrorPtr()使用全局错误指针,多线程下不安全。- 嵌套深度默认限制约 1000,防止栈溢出。
- 适合中小型 JSON,超大 JSON 建议用 yyjson、Jansson、json-c 等。
2. 获取与集成
源码集成:
从 GitHub 下载:
text
https://github.com/DaveGamble/cJSON
把 cJSON.c、cJSON.h 加入工程,包含:
c
#include "cJSON.h"
编译:
bash
gcc main.c cJSON.c -o main -lm
系统包安装,如 Ubuntu/Debian:
bash
sudo apt install libcjson-dev
包含:
c
#include <cjson/cJSON.h>
编译:
bash
gcc main.c -lcjson -o main
3. 核心数据结构
cJSON 节点大致定义如下:
c
typedef struct cJSON {
struct cJSON *next;
struct cJSON *prev;
struct cJSON *child;
int type;
char *valuestring;
int valueint; // 旧字段,已不推荐
double valuedouble;
char *string; // 对象成员的 key
} cJSON;
关系:
- 对象和数组都通过
child指向第一个子节点。 - 子节点之间通过
next/prev组成双向链表。 - 对象子节点的
string是键名。 - 数组子节点的
string通常为NULL。 type是位标志,常用类型:cJSON_FalsecJSON_TruecJSON_NULLcJSON_NumbercJSON_StringcJSON_ArraycJSON_ObjectcJSON_Raw
推荐用类型判断函数:
c
cJSON_IsObject(item);
cJSON_IsArray(item);
cJSON_IsString(item);
cJSON_IsNumber(item);
cJSON_IsBool(item);
cJSON_IsTrue(item);
cJSON_IsFalse(item);
cJSON_IsNull(item);
4. 解析 JSON 文件的标准流程
- 用
fopen(..., "rb")打开文件。 - 读取全部内容到内存。
- 在末尾补
'\0'。 - 如果文件有 UTF-8 BOM,跳过前 3 字节。
- 用
cJSON_Parse或cJSON_ParseWithOpts解析。 - 判断根节点类型。
- 用
cJSON_GetObjectItemCaseSensitive、cJSON_GetArrayItem等读取。 - 用
cJSON_Delete释放 JSON 树。 - 用
free释放自己读取的文件缓冲区。
5. 常用解析 API
c
cJSON *cJSON_Parse(const char *value);
cJSON *cJSON_ParseWithLength(const char *value, size_t buffer_length);
cJSON *cJSON_ParseWithOpts(const char *value,
const char **return_parse_end,
cJSON_bool require_null_terminated);
void cJSON_Delete(cJSON *item);
cJSON *cJSON_GetObjectItem(const cJSON *object, const char *string);
cJSON *cJSON_GetObjectItemCaseSensitive(const cJSON *object, const char *string);
cJSON *cJSON_GetArrayItem(const cJSON *array, int index);
int cJSON_GetArraySize(const cJSON *array);
double cJSON_GetNumberValue(const cJSON *item);
char *cJSON_GetStringValue(const cJSON *item);
const char *cJSON_GetErrorPtr(void);
建议:
- 对象取键用
cJSON_GetObjectItemCaseSensitive,避免大小写不敏感带来的意外。 - 解析时用
cJSON_ParseWithOpts(..., 1),要求整个输入是完整 JSON,防止尾随垃圾被忽略。 - 读取数字用
cJSON_GetNumberValue,不要依赖旧字段valueint。
6. 完整示例:读取并解析 JSON 文件
假设 config.json:
json
{
"name": "Alice",
"age": 30,
"skills": ["C", "Linux", "JSON"],
"nested": {
"enabled": true
}
}
C 代码:
c
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include "cJSON.h"
static char *read_file(const char *path, long *out_len) {
FILE *fp = fopen(path, "rb");
if (!fp) return NULL;
if (fseek(fp, 0, SEEK_END) != 0) {
fclose(fp);
return NULL;
}
long len = ftell(fp);
if (len < 0) {
fclose(fp);
return NULL;
}
rewind(fp);
char *buf = (char *)malloc((size_t)len + 1);
if (!buf) {
fclose(fp);
return NULL;
}
size_t n = fread(buf, 1, (size_t)len, fp);
fclose(fp);
buf[n] = '\0';
if (out_len) *out_len = (long)n;
return buf;
}
int main(int argc, char **argv) {
if (argc != 2) {
fprintf(stderr, "Usage: %s file.json\n", argv[0]);
return 1;
}
long len = 0;
char *text = read_file(argv[1], &len);
if (!text) {
perror("read_file");
return 1;
}
char *json = text;
// 跳过 UTF-8 BOM
if (len >= 3 &&
(unsigned char)json[0] == 0xEF &&
(unsigned char)json[1] == 0xBB &&
(unsigned char)json[2] == 0xBF) {
json += 3;
}
const char *end = NULL;
cJSON *root = cJSON_ParseWithOpts(json, &end, 1);
if (!root) {
const char *err = cJSON_GetErrorPtr();
long offset = end ? (long)(end - json) : -1;
fprintf(stderr, "JSON parse error at offset %ld near: %.40s\n",
offset, err ? err : "(unknown)");
free(text);
return 1;
}
// 读取字符串
cJSON *name = cJSON_GetObjectItemCaseSensitive(root, "name");
if (cJSON_IsString(name)) {
printf("name = %s\n", name->valuestring);
}
// 读取数字
cJSON *age = cJSON_GetObjectItemCaseSensitive(root, "age");
if (cJSON_IsNumber(age)) {
printf("age = %g\n", cJSON_GetNumberValue(age));
}
// 读取数组
cJSON *skills = cJSON_GetObjectItemCaseSensitive(root, "skills");
if (cJSON_IsArray(skills)) {
cJSON *item = NULL;
cJSON_ArrayForEach(item, skills) {
if (cJSON_IsString(item)) {
printf("skill: %s\n", item->valuestring);
}
}
}
// 读取嵌套对象
cJSON *nested = cJSON_GetObjectItemCaseSensitive(root, "nested");
if (cJSON_IsObject(nested)) {
cJSON *enabled = cJSON_GetObjectItemCaseSensitive(nested, "enabled");
if (cJSON_IsBool(enabled)) {
printf("nested.enabled = %s\n",
cJSON_IsTrue(enabled) ? "true" : "false");
}
}
cJSON_Delete(root);
free(text);
return 0;
}
编译:
bash
gcc main.c cJSON.c -o json_demo -lm
./json_demo config.json
7. 生成、修改、打印 JSON
创建:
c
cJSON *root = cJSON_CreateObject();
cJSON_AddStringToObject(root, "name", "Bob");
cJSON_AddNumberToObject(root, "age", 25);
cJSON_AddBoolToObject(root, "enabled", 1);
cJSON_AddNullToObject(root, "extra");
cJSON *arr = cJSON_AddArrayToObject(root, "skills");
cJSON_AddItemToArray(arr, cJSON_CreateString("C"));
cJSON_AddItemToArray(arr, cJSON_CreateString("JSON"));
char *out = cJSON_Print(root); // 格式化输出
// char *out = cJSON_PrintUnformatted(root); // 紧凑输出
printf("%s\n", out);
free(out); // 注意:cJSON_Print 返回的字符串用 free 释放
cJSON_Delete(root); // 释放 JSON 树
修改:
c
cJSON_ReplaceItemInObjectCaseSensitive(
root, "age", cJSON_CreateNumber(26)
);
cJSON_DeleteItemFromObjectCaseSensitive(root, "extra");
注意:
cJSON_AddItemToObject/cJSON_AddItemToArray后,节点所有权转移给父节点。cJSON_DetachItemFromObject分离出的节点需要自己cJSON_Delete。cJSON_Delete会递归释放整棵树。
8. 内存管理与错误处理
内存释放规则:
cJSON_Parse返回的树:用cJSON_Delete(root)。cJSON_Print返回的字符串:用free(),不是cJSON_Delete。- 自己
malloc的文件缓冲区:用free()。 - 加入父节点的节点:不要单独释放,最终释放根节点即可。
- 使用
cJSON_InitHooks自定义分配器时,释放要使用对应 hooks 的 free。
错误处理:
c
cJSON *root = cJSON_Parse(text);
if (!root) {
const char *err = cJSON_GetErrorPtr();
fprintf(stderr, "parse error near: %s\n", err ? err : "unknown");
}
更推荐:
c
const char *end = NULL;
cJSON *root = cJSON_ParseWithOpts(text, &end, 1);
if (!root) {
fprintf(stderr, "parse error at offset %ld\n", (long)(end - text));
}
注意:
cJSON_GetErrorPtr()返回全局指针,多线程不安全。- 解析失败后应尽快使用错误指针,因为下一次解析可能覆盖。
- 解析成功后,输入缓冲区可以释放;解析失败时,错误指针可能指向输入缓冲区,打印错误前不要释放输入。
9. 常见坑与注意事项
- 文件编码:JSON 标准要求 UTF-8。中文乱码通常是文件不是 UTF-8。
- BOM :UTF-8 BOM 会让 cJSON 解析失败,需要手动跳过
EF BB BF。 - 数字精度 :cJSON 用
double,超过 2^53 的整数可能不精确。 - 大小写 :
cJSON_GetObjectItem默认大小写不敏感,建议用cJSON_GetObjectItemCaseSensitive。 - 类型检查 :取到节点后先用
cJSON_IsString、cJSON_IsNumber等判断,再访问字段。 - 不要访问空指针 :
cJSON_GetObjectItemCaseSensitive找不到会返回NULL。 - 不支持注释 :
//、/* */、尾随逗号都不是标准 JSON,cJSON 解析器不接受。 - 线程安全:cJSON 本身大部分操作可并发,但错误指针和 hooks 是全局的。
- 大文件:cJSON 一次性加载,内存占用约为文件大小加节点开销。
- 释放方式 :树用
cJSON_Delete,打印字符串用free,不要混用。
10. 总结
用 cJSON 解析 JSON 文件的核心就是:
text
读文件 -> 补 '\0' -> 跳过 BOM -> cJSON_ParseWithOpts
-> 判断类型 -> GetObjectItem/GetArrayItem 遍历
-> 读取 valuestring / cJSON_GetNumberValue
-> cJSON_Delete 释放树 -> free 释放文件缓冲区
cJSON 简单、轻量、易集成,非常适合 C 语言项目解析配置文件和小型 JSON 数据。如果项目对性能、超大文件、流式解析或大整数精度要求很高,可以考虑 yyjson、Jansson、json-c 等替代库。
cJSON_ParseWithOpts使用
cJSON_ParseWithOpts 是 cJSON 中解析 JSON 的核心函数之一,它比 cJSON_Parse 提供了更精细的控制:可以要求整个输入必须是一个完整的 JSON 值(后面不能有多余的非空白字符),也可以返回解析结束的位置。下面详细介绍它的用法。
1. 函数原型
c
#include "cJSON.h"
CJSON_PUBLIC(cJSON *) cJSON_ParseWithOpts(
const char *value,
const char **return_parse_end,
cJSON_bool require_null_terminated
);
相关类型:
c
typedef int cJSON_bool;
#define cJSON_true 1
#define cJSON_false 0
2. 参数详解
2.1 const char *value
要解析的 JSON 字符串。
- 必须是有效的 C 字符串,以
'\0'结尾。 - 不能为
NULL,否则行为未定义(通常返回NULL并可能崩溃)。 - 如果 JSON 数据来自文件,需要先读入内存并补
'\0'。 - 如果文件有 UTF-8 BOM(
EF BB BF),需要手动跳过,否则解析会失败。
2.2 const char **return_parse_end
可选参数,用于返回解析结束的位置。
- 如果传入
NULL,则不关心结束位置。 - 如果传入一个
const char **变量的地址,函数成功时会把该指针设置为解析结束后的下一个字符位置。 - 具体指向哪里取决于
require_null_terminated:- 当
require_null_terminated = 1时,成功解析后函数会跳过尾随空白,然后检查是否到达'\0'。因此*return_parse_end会指向字符串末尾的'\0'。 - 当
require_null_terminated = 0时,函数解析完一个完整的 JSON 值就停止,不跳过后续字符。因此*return_parse_end指向 JSON 值之后的下一个字符,可能是空白、逗号、其他文本或'\0'。
- 当
- 解析失败时,
*return_parse_end也会被设置,通常指向出错位置附近,可用于计算错误偏移。
2.3 cJSON_bool require_null_terminated
是否要求整个输入必须"干净地"以 '\0' 结尾。
1(cJSON_true):严格模式。解析完一个 JSON 值后,函数会跳过空白字符(空格、\t、\n、\r),然后要求下一个字符必须是'\0'。如果还有任何非空白字符,解析失败并返回NULL。0(cJSON_false):宽松模式。函数只解析出第一个完整的 JSON 值,然后立即返回,不关心后面是否还有内容。
使用建议:
- 解析整个文件或网络包时,用
1,确保没有多余垃圾数据。 - 从一段混合文本中提取第一个 JSON 对象时,用
0。 - 连续解析多个 JSON 值时,用
0,并通过return_parse_end推进指针。
3. 返回值与错误处理
- 成功 :返回一个
cJSON *,指向解析出的 JSON 树根节点。使用完毕后必须用cJSON_Delete(root)释放。 - 失败 :返回
NULL。
失败时可通过以下方式获取错误信息:
c
const char *err = cJSON_GetErrorPtr();
cJSON_GetErrorPtr()返回一个指向错误位置附近的全局指针。- 该指针是全局的,多线程下不安全,应尽快使用。
- 如果使用了
return_parse_end,可以计算偏移量:
c
const char *end = NULL;
cJSON *root = cJSON_ParseWithOpts(text, &end, 1);
if (!root) {
long offset = end ? (long)(end - text) : -1;
fprintf(stderr, "parse error at offset %ld near: %.40s\n",
offset, end ? end : "(unknown)");
}
注意:解析失败时,text 缓冲区不要立即释放,因为 cJSON_GetErrorPtr() 或 end 可能指向它。
4. 与 cJSON_Parse 的关系
cJSON_Parse 是最简单的封装:
c
cJSON *cJSON_Parse(const char *value) {
return cJSON_ParseWithOpts(value, 0, 0);
}
也就是说:
- 不返回结束位置。
- 不要求整个字符串是完整 JSON,解析到第一个完整值就停止。
- 如果 JSON 后面有垃圾数据,
cJSON_Parse不会报错。
因此,在需要严格校验时,应直接使用 cJSON_ParseWithOpts。
5. 典型使用场景与示例
5.1 严格解析整个 JSON 文件
要求文件内容必须是一个完整的 JSON 值,后面只能有空白。
c
#include <stdio.h>
#include <stdlib.h>
#include "cJSON.h"
int main(void) {
FILE *fp = fopen("config.json", "rb");
if (!fp) { perror("fopen"); return 1; }
fseek(fp, 0, SEEK_END);
long len = ftell(fp);
rewind(fp);
char *text = malloc(len + 1);
fread(text, 1, len, fp);
fclose(fp);
text[len] = '\0';
// 跳过 UTF-8 BOM
char *json = text;
if (len >= 3 &&
(unsigned char)json[0] == 0xEF &&
(unsigned char)json[1] == 0xBB &&
(unsigned char)json[2] == 0xBF) {
json += 3;
}
const char *end = NULL;
cJSON *root = cJSON_ParseWithOpts(json, &end, 1);
if (!root) {
const char *err = cJSON_GetErrorPtr();
long offset = end ? (long)(end - json) : -1;
fprintf(stderr, "JSON parse error at offset %ld near: %.40s\n",
offset, err ? err : "(unknown)");
free(text);
return 1;
}
// 使用 root ...
cJSON_Delete(root);
free(text);
return 0;
}
5.2 宽松解析,获取结束位置
假设有一段文本,其中嵌入了一个 JSON 对象,后面还有其他内容:
c
const char *text = "{\"name\":\"Alice\",\"age\":30} 后面的内容";
const char *end = NULL;
cJSON *root = cJSON_ParseWithOpts(text, &end, 0);
if (root) {
printf("解析成功,JSON 结束于偏移 %ld\n", (long)(end - text));
printf("剩余内容: %s\n", end);
cJSON_Delete(root);
}
输出:
text
解析成功,JSON 结束于偏移 25
剩余内容: 后面的内容
注意:require_null_terminated = 0 时,end 指向 JSON 值后的第一个字符,这里是空格。
5.3 连续解析多个 JSON 值
如果一段文本中有多个连续的 JSON 值(例如 NDJSON,每行一个 JSON),可以用 return_parse_end 循环解析:
c
const char *p = "{\"a\":1}\n{\"b\":2}\n{\"c\":3}";
const char *end = NULL;
while (*p) {
// 跳过空白
while (*p == ' ' || *p == '\t' || *p == '\n' || *p == '\r') p++;
if (*p == '\0') break;
cJSON *item = cJSON_ParseWithOpts(p, &end, 0);
if (!item) {
fprintf(stderr, "解析失败,位置: %s\n", cJSON_GetErrorPtr());
break;
}
// 处理 item ...
char *out = cJSON_PrintUnformatted(item);
printf("解析到: %s\n", out);
free(out);
cJSON_Delete(item);
if (end == p) break; // 防止死循环
p = end;
}
5.4 校验解析后没有多余字符
严格模式已经自动完成这个校验。如果你想自己控制,可以用宽松模式加手动检查:
c
const char *end = NULL;
cJSON *root = cJSON_ParseWithOpts(text, &end, 0);
if (root) {
// 跳过尾随空白
while (*end == ' ' || *end == '\t' || *end == '\n' || *end == '\r') end++;
if (*end != '\0') {
fprintf(stderr, "JSON 后面有多余内容: %s\n", end);
cJSON_Delete(root);
root = NULL;
}
}
6. 注意事项与常见坑
-
value必须以'\0'结尾如果从文件读取时只读了
len字节,必须text[len] = '\0'。如果数据中包含'\0',应使用cJSON_ParseWithLength系列。 -
UTF-8 BOM 必须手动跳过
cJSON 不自动处理 BOM。跳过 3 字节
EF BB BF后再解析。 -
require_null_terminated = 1时,尾随空白是允许的空格、
\t、\n、\r会被跳过。其他任何字符都会导致失败。 -
return_parse_end在失败时也有效可以用来定位错误偏移。但不要依赖它一定指向精确的错误字符,它只是解析停止的位置。
-
错误指针是全局的
cJSON_GetErrorPtr()多线程不安全。如果多线程解析,建议只用return_parse_end计算偏移,或加锁。 -
内存释放
解析成功返回的树用
cJSON_Delete释放。return_parse_end只是指向原输入缓冲区的指针,不需要释放。 -
不要混用释放函数
cJSON_ParseWithOpts返回的树 →cJSON_DeletecJSON_Print返回的字符串 →free- 自己
malloc的文件缓冲区 →free
-
嵌套深度限制
cJSON 默认限制嵌套深度约 1000 层,超过会解析失败,防止栈溢出。
-
数字精度
内部用
double存储,大整数可能丢失精度。需要精确大整数时考虑其他库。 -
性能
一次性加载整个 JSON 到内存,适合中小型数据。超大 JSON 建议用流式解析器。
7. 总结
cJSON_ParseWithOpts 是 cJSON 解析功能的核心,掌握三个参数即可灵活应对不同场景:
| 参数 | 作用 | 常用取值 |
|---|---|---|
value |
要解析的 JSON 字符串 | 以 '\0' 结尾的缓冲区 |
return_parse_end |
返回解析结束位置 | NULL 或 const char ** |
require_null_terminated |
是否要求整个输入是完整 JSON | 1 严格,0 宽松 |
- 解析整个文件、配置、网络包 →
cJSON_ParseWithOpts(text, &end, 1) - 从混合文本提取第一个 JSON →
cJSON_ParseWithOpts(text, &end, 0) - 连续解析多个 JSON → 宽松模式 +
return_parse_end推进指针
正确使用它可以避免"JSON 后面有垃圾数据却解析成功"的隐蔽问题,也能方便地定位解析错误。