土法炼钢兴趣小组的算法知识备份

JSON 全面指南:历史、优缺点、竞争者,以及用 json-gen-c 在 C 语言中优雅地处理 JSON

目录

如果说互联网有一种”通用语言”,那一定是 JSON。几乎每一个现代 Web API 都在使用它,每一种主流编程语言都内置或拥有第三方 JSON 库,甚至连 NoSQL 数据库(如 MongoDB、CouchDB)都选择以 JSON(或其变体)作为原生存储格式。

然而,JSON 并非完美——它有着自己的局限性,也面临来自多种替代格式的竞争。更重要的是,在 C 语言这种没有反射机制、没有内置字符串类型的底层环境中,处理 JSON 一直是件苦差事。

本文将全面介绍 JSON 的历史、优缺点和竞争者,并重点展示如何使用 json-gen-c —— 一个先定义数据结构、再生成代码的 C 语言 JSON 工具 —— 来大幅简化 C 程序员处理 JSON 的工作流程。

1. JSON 简介与历史

1.1 什么是 JSON?

JSON(JavaScript Object Notation)是一种轻量级的数据交换格式。它基于 JavaScript 的对象字面量语法,但与语言无关,任何编程语言都可以轻松解析和生成 JSON 数据。

一段典型的 JSON 数据如下:

{
    "name": "张三",
    "age": 30,
    "is_programmer": true,
    "languages": ["C", "Go", "Python"],
    "address": {
        "city": "深圳",
        "country": "中国"
    }
}

JSON 的语法只有两种基本结构: - 对象(Object):由花括号 {} 包围的键值对集合 - 数组(Array):由方括号 [] 包围的有序值列表

值的类型可以是:字符串、数字、布尔值(true/false)、null、对象或数组。仅此而已。

1.2 历史演进

JSON 的诞生要从一个人说起——Douglas Crockford

2001 年,Douglas Crockford 注意到 JavaScript 的对象字面量语法足够简洁,可以直接用作数据交换格式。他在 json.org 上正式命名并描述了这种格式,JSON 就此诞生。

“I discovered JSON. I do not claim to have invented it, because it already existed in nature.” — Douglas Crockford

有趣的是,Crockford 一直强调他是”发现”了 JSON,而不是”发明”了它。因为 JavaScript 的对象字面量语法早在 1999 年(ECMAScript 3)就已存在,他只是给它起了个名字并推广为通用数据格式。

以下是 JSON 发展的关键时间线:

年份 里程碑
2001 Douglas Crockford 在 json.org 上提出 JSON
2002 JSON.org 网站上线,提供多语言解析器链接
2005 Ajax 技术兴起,JSON 作为 XML 的替代方案走入主流
2006 RFC 4627 发布,JSON 成为正式标准
2013 ECMA-404 发布,JSON 获得 ECMA 标准化
2014 RFC 7159 替代 RFC 4627,修正若干细节
2017 RFC 8259 发布,成为当前最新 JSON 标准

从时间线可以看出,JSON 从一个”民间约定”逐步登堂入室,历经三个 RFC 版本的迭代,成为了互联网上最广泛使用的数据交换格式之一。

1.3 JSON 为何杀死了 XML(在 Web API 领域)

2005 年之前,Web 服务的数据交换几乎被 XML 垄断(还记得 SOAP 吗?)。但 Ajax(Asynchronous JavaScript and XML)技术兴起后,开发者们很快注意到一件事:

<!-- XML 表示一个用户 -->
<user>
    <name>张三</name>
    <age>25</age>
    <active>true</active>
</user>
{"name": "张三", "age": 25, "active": true}

同样的数据,JSON 只需要 XML 大约 三分之一 的字符数。在带宽宝贵的年代,这个差距非常显著。更重要的是,在浏览器端解析 JSON 只需一行 JSON.parse(),而解析 XML 需要笨重的 DOM API。

到了 2010 年代,REST API 全面拥抱 JSON,XML 在 Web API 场景中的主导地位已明显被取代。

2. JSON 的优点

2.1 人类可读性

JSON 的语法设计得足够直观,即使是非程序员也能大致读懂。花括号代表对象,方括号代表数组,冒号分隔键值——这些符号的含义几乎不需要学习。

2.2 跨语言支持

截至目前,你能想到的几乎每一种编程语言都有成熟的 JSON 支持:

语言 内置/标准库 调用方式
JavaScript 内置 JSON.parse() / JSON.stringify()
Python 标准库 import json
Go 标准库 encoding/json
Java 常用第三方 Jackson / Gson
C++ 常用第三方 nlohmann/json / RapidJSON
Rust 常用第三方 serde_json
C 第三方 cJSON / Jansson / json-gen-c

2.3 轻量且高效

JSON 的语法元素极少(花括号、方括号、冒号、逗号、引号),没有 XML 那样的闭合标签冗余。以同样的数据量计算,JSON 体积通常只有 XML 的 30%~50%。

2.4 自描述性

JSON 数据本身就携带了结构信息(字段名),不需要额外的 schema 文件就能理解数据含义。这一点让它非常适合快速原型开发和 API 调试。

2.5 嵌套表达力

JSON 原生支持嵌套结构——对象中可以嵌套对象和数组,数组中可以嵌套对象和数组,理论上深度无限。这使它能表达复杂的层次化数据。

3. JSON 的缺点

JSON 远非完美,以下是它的主要痛点:

3.1 不支持注释

这是 JSON 被诟病最多的一点。Douglas Crockford 故意在 JSON 规范中排除了注释,理由是:

“I removed comments from JSON because I saw people were using them to hold parsing directives.”

这个决定导致 JSON 不太适合作为配置文件格式。你无法在一个复杂的 JSON 配置中添加任何解释性文字。实际项目中,许多团队被迫使用 JSONC(JSON with Comments)或干脆改用 YAML/TOML。

3.2 没有日期/时间类型

JSON 只有字符串、数字、布尔、null 四种基本类型,没有专用的日期/时间类型。你只能用字符串表示,再自行约定格式(ISO 8601?Unix 时间戳?还是某种自定义格式?)。不同系统之间的日期格式不一致是 JSON API 中的常见坑。

3.3 数字精度问题

JSON 规范并未限定数字的精度和范围。在实践中,由于大多数 JSON 解析器使用 IEEE 754 双精度浮点数(64 位),超过 \(2^{53}\) 的整数会丢失精度。Twitter 当年就遇到了这个问题,不得不同时返回数字和字符串两种形式的 tweet ID。

{
    "id": 9007199254740993,
    "id_str": "9007199254740993"
}

3.4 冗余度

每个对象中的每一条记录都要重复写字段名。如果你有一个包含 10000 条记录的数组,每条记录都有 "name""age""email" 这样的字段名,那光是字段名的重复就浪费了大量空间。

3.5 不支持二进制数据

JSON 是纯文本格式,无法直接嵌入二进制数据。要传输图片、文件等二进制内容,必须先 Base64 编码(体积增大约 33%),然后作为字符串嵌入。

3.6 缺乏标准 Schema

JSON 本身没有内建的 schema 机制。虽然 JSON Schema 规范存在,但它是可选的附加标准,远不如 XML Schema / DTD 在生态中的集成度。

4. JSON 的竞争者

JSON 通吃一切场景了吗?并非如此。不同场景下,有多种格式在与 JSON 竞争:

4.1 格式对比总览

格式 类型 人类可读 支持注释 Schema 支持 性能 适用场景
JSON 文本 [好] [否] 可选 (JSON Schema) Web API、配置、通用数据交换
XML 文本 [一般] [是] 强 (XSD/DTD) 企业集成、文档标记、SOAP
YAML 文本 [极好] [是] 可选 配置文件(K8s、Docker Compose)
TOML 文本 [好] [是] 简单配置文件(Cargo.toml)
Protobuf 二进制 [否] N/A 内建 (.proto) 极高 gRPC、高性能微服务通信
MessagePack 二进制 [否] N/A 高吞吐量的 JSON 替代方案
BSON 二进制 [否] N/A MongoDB 存储

4.2 各竞争者简评

4.3 该如何选择?

下面这张图提供一条简单的决策路径:

数据格式选择决策图

5. C 语言处理 JSON:一个传统难题

前面说了 JSON 的种种好处,但对 C 程序员来说,使用 JSON 一直是一种折磨。原因很简单:

5.1 C 没有反射机制

像 Go 的 encoding/json 或 Python 的 json 模块可以自动遍历数据结构的字段并映射到 JSON 键名。C 做不到——编译后,所有结构体字段名信息就丢失了。

5.2 手写解析代码极其繁琐

假设你有这样一个结构体:

struct User {
    int id;
    char name[64];
    double balance;
};

使用常见的 C JSON 库(如 cJSON)来序列化和反序列化,你需要这样写:

// --- 序列化 ---
cJSON *root = cJSON_CreateObject();
cJSON_AddNumberToObject(root, "id", user.id);
cJSON_AddStringToObject(root, "name", user.name);
cJSON_AddNumberToObject(root, "balance", user.balance);
char *json_str = cJSON_Print(root);
cJSON_Delete(root);

// --- 反序列化 ---
cJSON *root = cJSON_Parse(json_str);
cJSON *id_item = cJSON_GetObjectItem(root, "id");
if (id_item) user.id = id_item->valueint;
cJSON *name_item = cJSON_GetObjectItem(root, "name");
if (name_item) {
    strncpy(user.name, name_item->valuestring, sizeof(user.name) - 1);
    user.name[sizeof(user.name) - 1] = '\0';
}
cJSON *balance_item = cJSON_GetObjectItem(root, "balance");
if (balance_item) user.balance = balance_item->valuedouble;
cJSON_Delete(root);

才三个字段就已经这么多代码了。如果是一个有 20 个字段、其中还包含嵌套结构和数组的复杂类型呢?光是反序列化代码就可能长达数百行,而且每一行都是手写的、容易出错的。

5.3 错误处理是噩梦

漏了一个 NULL 检查?嵌套层级写错了?字段名拼写有误?——这些 bug 在编译期完全不会暴露,只会在运行时悄悄地给你处理出错误数据或段错误。


那么,有没有一种方式能让 C 程序员像 Go 语言那样,只定义数据结构,就自动获得全部的 JSON 序列化/反序列化能力呢?

答案是 json-gen-c

6. json-gen-c:让 C 语言也能优雅地处理 JSON

json-gen-c 是一个代码生成器,它的理念很简单:

只描述一次数据结构,自动生成全部 JSON 操作代码。

6.1 核心设计理念

特性 说明
先定义结构,再生成代码 .json-gen-c 文件中定义结构体,一次描述,自动生成 C 代码
零运行时反射 所有映射逻辑在代码生成阶段确定,运行时无额外开销
线程安全 解析上下文使用显式结构体而非全局变量
配套齐全 自带轻量级 sstr 字符串库和数组辅助函数
便于接入 CI 警告即错误,Make 目标在本地和 CI 环境行为一致

6.2 工作流程

整个流程只需三步:

json-gen-c 工作流程
  1. 定义:在 .json-gen-c 文件中用类 C 语法描述数据结构
  2. 生成:运行 json-gen-c 命令,自动生成 .h.c 文件
  3. 使用:在你的代码中 #include "json.gen.h",直接调用生成的函数

6.3 安装

git clone https://github.com/zltl/json-gen-c.git
cd json-gen-c
make -j$(nproc)
sudo make install

6.4 支持的数据类型

类型 说明
int 整数
long 长整数
float 单精度浮点数
double 双精度浮点数
bool 布尔值
sstr_t 动态字符串(安全替代 char*
自定义结构体 嵌套结构体引用
以上类型 + [] 动态数组

6.5 生成的 API

对于每个定义的结构体 Foo,json-gen-c 自动生成以下函数:

// 初始化结构体(将所有字段置零/NULL)
int Foo_init(struct Foo *obj);

// 清理结构体(释放所有动态分配的内存)
int Foo_clear(struct Foo *obj);

// 序列化:结构体 -> JSON 字符串
int json_marshal_Foo(struct Foo *obj, sstr_t out);

// 序列化(带缩进的美观输出)
int json_marshal_indent_Foo(struct Foo *obj, int indent, int curindent, sstr_t out);

// 反序列化:JSON 字符串 -> 结构体
int json_unmarshal_Foo(sstr_t in, struct Foo *obj);

// 数组序列化 / 反序列化
int json_marshal_array_Foo(struct Foo *obj, int len, sstr_t out);
int json_unmarshal_array_Foo(sstr_t in, struct Foo **obj, int *len);

完整的 API 文档请参考:json-gen-c Doxygen 文档

6.6 适用边界

json-gen-c 最适合结构稳定、类型明确、能够在编译前确定 JSON 完整字段布局的场景。典型的适用场景包括:协议消息、配置文件、游戏存档、设备上报数据等。

以下场景可能不太适合纯代码生成方案:

对于这类场景,cJSON、Jansson 等运行时解析库仍然是合理的选择,json-gen-c 并不试图取代它们。

7. 完整实战示例:RPG 游戏角色存档系统

接下来用一个完整的项目——为一款 RPG 游戏实现角色存档的 JSON 序列化——来看看 json-gen-c 的实际效果。

之所以选择这个示例,是因为它同时覆盖了 C 语言处理 JSON 时的几乎所有典型难点:标量字段(intdouble)、动态字符串(sstr_t)、嵌套结构体(角色→位置)、动态数组(背包物品列表)以及字符串数组(成就列表)的内存管理。如果 json-gen-c 能干净利落地处理这个结构,那日常遇到的绝大多数 JSON 场景它都能覆盖。

7.1 设计数据结构

一个角色存档需要保存: - 角色信息:名字、等级、HP/MP、职业 - 位置信息:地图坐标 (x, y) - 背包物品:物品名称、类型、数量、属性值 - 存档元信息:存档名、版本号

创建文件 game_save.json-gen-c

// 地图位置
struct Position {
    double x;
    double y;
};

// 背包中的物品
struct Item {
    sstr_t name;
    sstr_t type;
    int quantity;
    double power;
};

// 游戏角色
struct Character {
    sstr_t name;
    sstr_t class_name;
    int level;
    int hp;
    int max_hp;
    int mp;
    int max_mp;
    double attack;
    double defense;
    Position position;
    Item inventory[];
};

// 存档主结构
struct GameSave {
    sstr_t save_name;
    int version;
    long play_time_seconds;
    Character player;
    sstr_t achievements[];
};

这段定义文件展示了 json-gen-c 的全部表达能力:标量字段(intdouble)、字符串(sstr_t)、嵌套结构体(PositionCharacter)和动态数组(Item inventory[]sstr_t achievements[])。

7.2 生成代码

json-gen-c -in game_save.json-gen-c -out .

这一条命令会生成: - json.gen.h — 结构体定义和函数声明 - json.gen.c — 全部序列化/反序列化实现 - sstr.h / sstr.c — 字符串辅助库

7.3 编写主程序

创建 main.c(为突出核心流程,以下示例省略了 malloc 返回值检查等错误处理,生产代码中应补全):

#include <stdio.h>
#include <stdlib.h>
#include "json.gen.h"
#include "sstr.h"

int main() {
    // ========================================
    // 1. 创建游戏存档
    // ========================================
    struct GameSave save;
    GameSave_init(&save);

    save.save_name = sstr("英雄之旅-存档1");
    save.version = 2;
    save.play_time_seconds = 36000; // 10 小时

    // 设置角色信息
    save.player.name = sstr("亚瑟王");
    save.player.class_name = sstr("圣骑士");
    save.player.level = 42;
    save.player.hp = 850;
    save.player.max_hp = 1000;
    save.player.mp = 320;
    save.player.max_mp = 500;
    save.player.attack = 156.5;
    save.player.defense = 203.8;

    // 设置位置
    save.player.position.x = 1024.5;
    save.player.position.y = -768.3;

    // 设置背包物品(3 件)
    save.player.inventory_len = 3;
    save.player.inventory = (struct Item *)malloc(
        sizeof(struct Item) * save.player.inventory_len);

    // 物品 1:圣光之剑
    Item_init(&save.player.inventory[0]);
    save.player.inventory[0].name = sstr("圣光之剑");
    save.player.inventory[0].type = sstr("weapon");
    save.player.inventory[0].quantity = 1;
    save.player.inventory[0].power = 285.0;

    // 物品 2:生命药水
    Item_init(&save.player.inventory[1]);
    save.player.inventory[1].name = sstr("生命药水");
    save.player.inventory[1].type = sstr("consumable");
    save.player.inventory[1].quantity = 15;
    save.player.inventory[1].power = 200.0;

    // 物品 3:魔法盾牌
    Item_init(&save.player.inventory[2]);
    save.player.inventory[2].name = sstr("魔法盾牌");
    save.player.inventory[2].type = sstr("armor");
    save.player.inventory[2].quantity = 1;
    save.player.inventory[2].power = 180.5;

    // 设置成就
    save.achievements_len = 3;
    save.achievements = (sstr_t *)malloc(
        sizeof(sstr_t) * save.achievements_len);
    save.achievements[0] = sstr("初次冒险");
    save.achievements[1] = sstr("屠龙勇士");
    save.achievements[2] = sstr("百战不殆");

    // ========================================
    // 2. 序列化为 JSON(带缩进美化输出)
    // ========================================
    sstr_t json_out = sstr_new();
    json_marshal_indent_GameSave(&save, 4, 0, json_out);

    printf("=== 游戏存档 JSON ===\n%s\n", sstr_cstr(json_out));

    // ========================================
    // 3. 从 JSON 反序列化回结构体
    // ========================================
    struct GameSave loaded_save;
    GameSave_init(&loaded_save);
    int ret = json_unmarshal_GameSave(json_out, &loaded_save);

    if (ret == 0) {
        printf("\n=== 存档加载成功 ===\n");
        printf("存档名: %s\n", sstr_cstr(loaded_save.save_name));
        printf("角色: %s (Lv.%d %s)\n",
               sstr_cstr(loaded_save.player.name),
               loaded_save.player.level,
               sstr_cstr(loaded_save.player.class_name));
        printf("HP: %d/%d  MP: %d/%d\n",
               loaded_save.player.hp, loaded_save.player.max_hp,
               loaded_save.player.mp, loaded_save.player.max_mp);
        printf("位置: (%.1f, %.1f)\n",
               loaded_save.player.position.x,
               loaded_save.player.position.y);
        printf("背包物品: %d\n", loaded_save.player.inventory_len);

        int i;
        for (i = 0; i < loaded_save.player.inventory_len; i++) {
            printf("  - %s (%s) x%d  威力:%.1f\n",
                   sstr_cstr(loaded_save.player.inventory[i].name),
                   sstr_cstr(loaded_save.player.inventory[i].type),
                   loaded_save.player.inventory[i].quantity,
                   loaded_save.player.inventory[i].power);
        }

        printf("成就: ");
        for (i = 0; i < loaded_save.achievements_len; i++) {
            printf("[%s] ", sstr_cstr(loaded_save.achievements[i]));
        }
        printf("\n");
    } else {
        printf("存档加载失败!\n");
    }

    // ========================================
    // 4. 清理资源
    // ========================================
    sstr_free(json_out);
    GameSave_clear(&loaded_save);
    GameSave_clear(&save);

    return 0;
}

7.4 编译与运行

# 生成代码
json-gen-c -in game_save.json-gen-c -out .

# 编译
gcc -o game_save main.c json.gen.c sstr.c -std=c11

# 运行
./game_save

7.5 运行结果

程序输出的 JSON 存档数据如下(带 4 空格缩进的美观格式):

=== 游戏存档 JSON ===
{
    "save_name": "英雄之旅-存档1",
    "version": 2,
    "play_time_seconds": 36000,
    "player": {
        "name": "亚瑟王",
        "class_name": "圣骑士",
        "level": 42,
        "hp": 850,
        "max_hp": 1000,
        "mp": 320,
        "max_mp": 500,
        "attack": 156.500000,
        "defense": 203.800000,
        "position": {
            "x": 1024.500000,
            "y": -768.300000
        },
        "inventory": [
            {
                "name": "圣光之剑",
                "type": "weapon",
                "quantity": 1,
                "power": 285.000000
            },
            {
                "name": "生命药水",
                "type": "consumable",
                "quantity": 15,
                "power": 200.000000
            },
            {
                "name": "魔法盾牌",
                "type": "armor",
                "quantity": 1,
                "power": 180.500000
            }
        ]
    },
    "achievements": [
        "初次冒险",
        "屠龙勇士",
        "百战不殆"
    ]
}

=== 存档加载成功 ===
存档名: 英雄之旅-存档1
角色: 亚瑟王 (Lv.42 圣骑士)
HP: 850/1000  MP: 320/500
位置: (1024.5, -768.3)
背包物品: 3 件
  - 圣光之剑 (weapon) x1  威力:285.0
  - 生命药水 (consumable) x15  威力:200.0
  - 魔法盾牌 (armor) x1  威力:180.5
成就: [初次冒险] [屠龙勇士] [百战不殆]

可以看到,我们没有手写任何解析或序列化逻辑——只需定义数据结构,常规的 JSON 序列化/反序列化代码就由 json-gen-c 自动生成了。

8. 与传统方式的代码量对比

下面对比一下,实现同样的”游戏存档序列化/反序列化”功能,使用 cJSON 手写需要多少代码。

以下对比基于同一 RPG 存档示例的手写代码量估算,不含自动生成的代码和测试代码:

8.1 使用 json-gen-c

内容 代码行数
结构体定义(.json-gen-c ~35 行
主程序(创建数据 + 序列化 + 反序列化 + 打印) ~100 行
手写 JSON 处理代码 0 行
总计(需要手写的代码) ~135 行

8.2 使用 cJSON 手写

内容 代码行数
结构体定义(.h 文件中) ~35 行
序列化函数(struct -> cJSON -> string) ~80 行
反序列化函数(string -> cJSON -> struct) ~120 行
init / clear 函数 ~40 行
错误处理代码 ~30 行
主程序 ~100 行
总计(需要手写的代码) ~405 行

直观对比:

json-gen-c 与 cJSON 代码量对比

json-gen-c 将手动编写代码量减少了约 67%,并显著降低了手写序列化逻辑中常见的拼写错误、类型不匹配、遗漏字段等 bug 风险。

但代码行数只是表面收益。真正更重要的差异体现在结构演进时的维护成本:如果明天你的结构体需要新增一个字段,json-gen-c 只需在定义文件中加一行然后重新生成——改动恰好一处。而 cJSON 的方案则需要同步修改序列化函数、反序列化函数、init 函数和 clear 函数——四处修改,四处可能出错。随着项目迭代,这种”改一处 vs 改四处”的差距会被不断放大,这才是代码生成方案最核心的工程价值。

9. 进阶:数组批量操作

json-gen-c 还支持结构体数组级别的序列化和反序列化。例如,保存多个角色存档:

// 批量序列化
struct GameSave saves[3];
// ... 分别初始化 saves[0], saves[1], saves[2] ...

sstr_t json_out = sstr_new();
json_marshal_array_GameSave(saves, 3, json_out);
printf("%s\n", sstr_cstr(json_out));

// 批量反序列化
struct GameSave *loaded = NULL;
int count = 0;
json_unmarshal_array_GameSave(json_out, &loaded, &count);

printf("已加载 %d 个存档\n", count);
int i;
for (i = 0; i < count; i++) {
    printf("存档 %d: %s\n", i + 1, sstr_cstr(loaded[i].save_name));
    GameSave_clear(&loaded[i]);
}
free(loaded);
sstr_free(json_out);

这在实现“存档列表”、“排行榜数据”等功能时非常实用。

10. 总结

JSON 的地位

尽管有着不支持注释、缺乏日期类型、数字精度问题等缺陷,JSON 凭借其极致的简洁、广泛的跨语言支持和良好的人类可读性,仍然是当今互联网上最通用的数据交换格式之一。在可见的主流 Web 场景中,JSON 仍将长期占据主导地位。

选择合适的格式

不同场景有不同的最佳选择: - Web API – JSON - 高性能通信 – Protobuf / MessagePack - 配置文件 – TOML(简单)/ YAML(复杂) - 企业集成 – XML - 数据库存储 – BSON / JSON

json-gen-c 的价值

对于 C 程序员来说,json-gen-c 提供了一条优雅的道路:

json-gen-c 不是要把 C 变成动态语言,而是把那些重复、机械、易错的 JSON 样板代码提前转移到代码生成阶段——让你在写 C 的时候,只需要关心数据本身。

如果你正在用 C 语言开发需要处理 JSON 的项目——无论是嵌入式设备上的配置解析、游戏引擎中的数据存储,还是高性能服务器的协议处理——不妨试试 json-gen-c。

项目资源:


By .