嵌入式学习之插件-CJSON库
嵌入式JSON封神库!cJSON从入门到精通,告别手写状态机的痛苦
做嵌入式联网、物联网、车载开发的工程师,几乎都踩过同一个坑:
设备上云对接MQTT,订阅主题收到云端下发的JSON——嵌套对象里套数组,数组里又塞对象,还掺着几个带\"和\\的转义字符串。
常规的做法: 打开Keil撸了个switch-case状态机,改到第三版,转义字符的边界条件还是崩了,调试到半夜怀疑人生。

直到某天打开GitHub才发现,圈里早就有个“人手一份”的老库,作者2009年就开始维护,如今已经12.6k Star、3.5k Fork、49个Release,接进工程只需要复制两个文件——cJSON.c和cJSON.h。
它就是cJSON,嵌入式JSON领域的“缺省答案”,今天就从入门到精通,把它讲透,让你再也不用手写JSON解析器!
Part.01
嵌入式JSON的“轻量王者”

cJSON是一个用ANSI C(C89)编写的超轻量级JSON解析器,作者Dave Gamble给它的定位很“佛系”:“能把活干完的最笨的那种解析器”。
这句话看似谦虚,实则是最精准的工程哲学——不追花哨特性,不搞复杂抽象,接口就那几个,但每一个都扎实能用,完美适配嵌入式的资源限制。
cJSON核心信息一览(必记):
-
作者:Dave Gamble(原作者),Max Bruckner、Alan Wang(当前维护者)
-
代码量:1个.c文件 + 1个.h文件,无多余依赖,可直接复制使用
-
语言标准:ANSI C(C89),兼容Keil、IAR、STM32CubeIDE等所有嵌入式编译器
-
许可证:MIT协议,商用零负担,不用担心版权问题
-
最新版本:v1.7.19(2025年9月)
-
仓库地址:https://github.com/DaveGamble/cJSON(建议star收藏)
为什么嵌入式圈对它情有独钟?
如今JSON在嵌入式里早已不是“可选项”——MQTT消息载荷、HTTP REST对接、云平台上报、设备配置下发、本地OTA配置、Modbus扩展协议,到处都是JSON的身影。
而cJSON的优势恰好戳中嵌入式工程师的痛点:可移植、可审计、可替换内存分配器、可裁剪,哪怕是资源不算富裕的MCU,也能轻松跑起来。
Part.02
设计思路:为什么它能只用两个文件?

打开cJSON的源码,你会发现一个核心逻辑:把复杂度往数据结构里藏。整个库的所有功能,都围绕一个极简的结构体展开:
typedef struct cJSON {struct cJSON *next; // 双向链表:下一个节点struct cJSON *prev; // 双向链表:上一个节点struct cJSON *child; // 子节点:用于嵌套(对象/数组)int type; // 类型标志(bit-flag),区分7种JSON类型char *valuestring; // 字符串值(string类型用)int valueint; // 整数值(已废弃,用cJSON_SetNumberValue替代)double valuedouble; // 数值(number类型用)char *string; // 键名(对象的key用)} cJSON;
很多人会疑惑:JSON有null、true、false、number、string、array、object七种类型,为什么这里只用一个结构体?
答案藏在type字段里——它是位标志(bit-flag)。同一个结构体,通过type的不同位区分七种JSON类型;再通过next/prev组成双向链表,用来表示数组和对象的成员;通过child指针形成树状结构,实现JSON的嵌套。
这种设计有一个小代价:按索引访问数组是O(n²)复杂度(因为是链表遍历),所以官方专门提供了cJSON_ArrayForEach宏,实现O(n)高效遍历。
但对嵌入式场景来说,这个取舍非常值:
-
不需要动态数组、哈希表,内存占用可控
-
内存分配次数少,适合嵌入式资源紧张的场景
-
代码量极小,全部源码可人工审计,调试方便
-
逻辑简单,任何嵌入式工程师读10分钟源码,就能看懂它的运行原理
Part.03
核心能力:四类API,覆盖所有日常场景

cJSON的API设计极其简洁,核心就四类:解析、构建、遍历、类型判断,学会这四类,就能应对嵌入式里99%的JSON场景。
3.1 解析:一行代码,搞定JSON字符串解析
最常用的场景:收到MQTT消息、HTTP响应等JSON字符串,把它转换成可访问的数据结构。
// 核心解析函数:将JSON字符串转为cJSON对象树cJSON *json = cJSON_Parse(string);// 解析失败判断(必做,避免空指针崩溃)if (json == NULL) {const char *err = cJSON_GetErrorPtr(); // 获取解析错误位置if (err != NULL) {printf("解析错误位置: %s\n", err);}return -1;}// ... 这里使用解析后的json对象 ...cJSON_Delete(json); // 用完必须释放,避免内存泄漏
补充说明:
-
如果字符串不一定是零终止(比如从socket直接接收的buffer),用
cJSON_ParseWithLength(string, len)更安全,指定字符串长度。 -
多线程场景下,
cJSON_GetErrorPtr()有竞态风险,推荐用cJSON_ParseWithOpts(value, &return_parse_end, 1),通过return_parse_end返回错误位置,线程安全。
3.2 构建:先Create,再Add,轻松生成JSON
反向场景:设备上报数据、下发指令,需要构建JSON字符串发送给云端或其他设备。核心逻辑:先创建根对象,再往里面添加字段、数组、子对象。
// 1. 创建根对象(JSON对象)cJSON *root = cJSON_CreateObject();// 2. 往根对象添加字段(字符串、数字、布尔值)cJSON_AddStringToObject(root, "device", "STM32F407"); // 字符串cJSON_AddNumberToObject(root, "temperature", 25.6); // 数字cJSON_AddBoolToObject(root, "online", 1); // 布尔值(1=true,0=false)// 3. 添加数组(比如设备传感器列表)cJSON *sensors = cJSON_AddArrayToObject(root, "sensors");// 往数组添加元素cJSON_AddItemToArray(sensors, cJSON_CreateNumber(1));cJSON_AddItemToArray(sensors, cJSON_CreateNumber(2));cJSON_AddItemToArray(sensors, cJSON_CreateNumber(3));// 4. 生成JSON字符串(紧凑格式,适合网络传输)char *out = cJSON_PrintUnformatted(root);printf("%s\n", out);// 5. 释放资源(必须做,避免内存泄漏)free(out); // 生成的字符串需要手动释放cJSON_Delete(root); // 根对象释放,整个对象树都会被释放
极其容易踩的坑(必看):
一个cJSON*节点,一旦被AddItemToObject或AddItemToArray加到父节点,所有权就交给父节点了,你不能再对它单独调用cJSON_Delete,否则会导致double free(双重释放)。只要删除根节点,整棵对象树都会被一起释放。
3.3 遍历:ArrayForEach宏,高效遍历数组/对象

前面提到,数组是链表结构,按索引访问效率低(O(n²)),官方提供了cJSON_ArrayForEach宏,实现O(n)高效遍历,同时适配数组和对象(对象内部也是链表存储)。
// 1. 获取数组(假设root里有一个叫"sensors"的数组)cJSON *array = cJSON_GetObjectItemCaseSensitive(root, "sensors");// 2. 遍历数组cJSON *item = NULL;cJSON_ArrayForEach(item, array) {// 判断元素类型,获取值if(cJSON_IsNumber(item)) {printf("传感器ID: %f\n", item->valuedouble);}}
如果是遍历对象,逻辑完全一样,把数组换成对象即可:
cJSON *obj = cJSON_GetObjectItemCaseSensitive(root, "device_info");cJSON *item = NULL;cJSON_ArrayForEach(item, obj) {// item->string 是键名,item->valuestring/valuedouble 是值printf("%s: %s\n", item->string, item->valuestring);}
3.4 类型判断:永远用Is系列函数,避免踩坑

type == cJSON_Number重点提醒:不要直接比较!
因为type是位标志,可能叠加cJSON_IsReference等标志位,直接比较会判断错误。官方提供的cJSON_IsXXX系列函数,会自动做NULL检查 + 类型检查,安全又省心。
// 正确写法:获取键值并判断类型cJSON *name = cJSON_GetObjectItemCaseSensitive(root, "device");if (cJSON_IsString(name) && (name->valuestring != NULL)) {printf("设备名称: %s\n", name->valuestring);}
这个写法还有一个好处:cJSON_GetObjectItemCaseSensitive允许NULL输入并安全返回NULL,cJSON_IsString收到NULL也会直接返回0,不需要层层写NULL判断,代码更简洁。
Part.04
上手最小示例:3分钟跑起来,零门槛集成
cJSON的集成难度几乎为零,最暴力的方式:复制cJSON.c和cJSON.h两个文件到工程,编译通过就能用,兼容Keil、IAR、STM32CubeIDE、PlatformIO等所有嵌入式开发环境。
下面是一个完整的最小示例,新建json_test.c,直接复制就能运行:
#include<stdio.h>#include<stdlib.h>#include"cJSON.h"intmain(void){// 模拟MQTT收到的JSON payloadconst char *payload ="{"" \"device\":\"mcu-01\","" \"temp\":26.5,"" \"alerts\":[\"high_temp\",\"low_battery\"]""}";// 1. 解析JSONcJSON *root = cJSON_Parse(payload);if (!root) {return -1; // 解析失败直接返回}// 2. 获取各个字段并打印cJSON *device = cJSON_GetObjectItemCaseSensitive(root, "device");cJSON *temp = cJSON_GetObjectItemCaseSensitive(root, "temp");cJSON *alerts = cJSON_GetObjectItemCaseSensitive(root, "alerts");if (cJSON_IsString(device)) {printf("设备ID: %s\n", device->valuestring);}if (cJSON_IsNumber(temp)) {printf("当前温度: %.1f\n", temp->valuedouble);}// 3. 遍历alerts数组cJSON *alert;cJSON_ArrayForEach(alert, alerts) {if (cJSON_IsString(alert)) {printf("告警信息: %s\n", alert->valuestring);}}// 4. 释放资源cJSON_Delete(root);return 0;}
编译运行(Linux/macOS):
gcc -std=c89 cJSON.c json_test.c -o json_test./json_test
输出结果:
设备ID: mcu-01当前温度: 26.5告警信息: high_temp告警信息: low_battery
Keil集成更简单:把两个文件加到工程分组,配置好头文件路径,直接#include "cJSON.h"就能用,不需要额外配置。
补充优化:如果想节省一次malloc(嵌入式内存宝贵),可以用cJSON_PrintPreallocated(item, buffer, length, format),把JSON输出到自己预留的静态缓冲区,完全绕过动态分配,适合MCU场景。
Part.05
嵌入式重点:自定义内存分配器
嵌入式开发中,很多场景不用标准库的malloc和free,比如FreeRTOS用pvPortMalloc、RT-Thread用rt_malloc,或者自己实现内存池。
cJSON贴心地留了全局Hook(钩子),可以轻松替换内存分配器,让cJSON的内存操作和你的工程保持一致。
// 1. 定义自己的内存分配和释放函数(示例:FreeRTOS)void *my_malloc(size_t size) {return pvPortMalloc(size);}void my_free(void *ptr) {vPortFree(ptr);}// 2. 初始化cJSON的内存Hook(必须在所有cJSON调用之前)void cjson_init(void) {cJSON_Hooks hooks;hooks.malloc_fn = my_malloc; // 自定义分配函数hooks.free_fn = my_free; // 自定义释放函数cJSON_InitHooks(&hooks);}
注意事项:
cJSON_InitHooks必须在所有cJSON函数调用之前设置好,之后不能再修改,否则会导致线程不安全。通常建议放在main函数开头,初始化一次即可。
这个机制的价值:让cJSON全部走RTOS的内存池,避免和libc的堆冲突;也可以接到自己的内存池里,方便做内存使用统计和泄漏监控,嵌入式工程里这个能力非常重要。
Part.06
同类选型:什么时候选cJSON,什么时候不选?
JSON在C语言生态里不止cJSON一个选择,不同场景适合不同的库,这里做一个清晰的选型参考,避免用错工具:
|
应用场景 |
推荐库 |
选择原因 |
|---|---|---|
|
资源正常的MCU(Cortex-M3/M4/M7),做MQTT/HTTP对接 |
cJSON |
API友好、构建/输出方便、调试简单,兼顾轻量和实用性 |
|
资源极紧(Cortex-M0,RAM不到8KB) |
jsmn |
不建对象树,只返回token偏移,内存占用极低(几KB) |
|
Linux/RTOS上做复杂业务,需要严格类型检查 |
Jansson / json-c |
功能更全、错误处理更完善,适合复杂场景 |
|
只读JSON、不生成JSON |
parson / jsmn |
比cJSON更轻,专注解析功能,资源占用更少 |
|
需要JSON Schema、JSON Patch功能 |
cJSON + cJSON_Utils |
cJSON官方附带的cJSON_Utils库,支持JSON Pointer和Patch |
一句话总结:
如果你在做ESP32、STM32F4/F7/H7这类中高端MCU的联网项目,cJSON几乎是默认答案;如果你在Cortex-M0里用16KB RAM挣扎,优先考虑jsmn。
Part.07
局限性:别把cJSON神化,这些坑要避开
cJSON虽好,但不是万能的。官方文档里坦率列出了它的局限性,工程上踩过坑的都懂,提前了解能少走很多弯路:
-
\0不支持字符串内部的字符:因为cJSON的字符串是零终止的,一旦遇到\0就会认为字符串结束。如果协议里要传二进制数据,不能直接塞进JSON的string字段,必须先做Base64编码。 -
仅支持UTF-8编码:大部分情况下不会主动拒绝非法UTF-8,只会原样透传。如果输入的JSON是GBK编码,解析会出问题,需要提前转成UTF-8。
-
浮点数仅保证IEEE754双精度:其他浮点数实现可能能跑,但不承诺正确。另外,浮点字面量的最大长度是63字符,超过会解析失败。
-
嵌套深度限制1000层:默认通过
CJSON_NESTING_LIMIT限制嵌套深度为1000,防止递归解析导致栈溢出(嵌入式栈本来就小),可在编译时修改。 -
默认不线程安全:只有满足3个条件才线程安全:不使用
cJSON_GetErrorPtr、cJSON_InitHooks只在主线程启动前调用、所有cJSON调用期间不调用setlocale。多线程场景建议加互斥锁。 -
默认不区分大小写:历史原因,原始API不区分键名的大小写(比如“Device”和“device”会被认为是同一个键)。严格遵循JSON标准,一定要用带
CaseSensitive后缀的函数(如cJSON_GetObjectItemCaseSensitive)。 -
按索引访问数组效率低(O(n²)):链表结构的固有缺陷,几百个元素的数组遍历,务必用
cJSON_ArrayForEach宏。 -
重复key只返回第一个:JSON标准允许对象有重复成员,cJSON解析不会报错,但
GetObjectItem只会返回第一个key的值。如果协议要求key唯一,需要在应用层自己校验。
这些局限性在嵌入式日常场景(如设备上云、简单数据上报)中大多无伤大雅,但如果用它做网关、中间件、跨语言协议层,一定要提前规避这些问题。
Part.08
总结:嵌入式工程师的必备工具
cJSON之所以能在嵌入式圈“人手一份”,核心在于它的“务实”——不追求完美,却能精准解决嵌入式JSON解析/构建的核心痛点:轻量、可移植、易用、商用无负担。
对于大多数嵌入式联网项目来说,cJSON的功能足够用,而且学习成本极低,花1小时学会核心API,就能告别手写状态机的痛苦,把时间花在更核心的业务逻辑上。
最后提醒:收藏cJSON的GitHub仓库,遇到问题看官方文档(最权威);集成时记得复制两个文件,用完释放资源,避开文中提到的坑,就能轻松上手。
到此,cJSON库的相关知识将讲解完毕了,后续需要大家多多训练
如果这篇教程对你有帮助,记得点赞 + 收藏!关注我,STM32 开发少走弯路

|
|
夜雨聆风