JSON for Modern C++ 中的 BJData 反序列化:nlohmann::json::from_bjdata 全面解析与实战
from_bjdata 是 nlohmann/json(JSON for Modern C++)提供的静态反序列化接口,负责把遵循 BJData(Binary JData) 规范的二进制字节流还原为 json 值。本文基于官方 API 文档与仓库源码,系统讲解两个重载的签名与参数语义、底层解析实现、异常与错误处理,并结合类型映射、ND-array 注释格式、可运行示例与单元测试,帮助你在消息传输、科学计算数组打包、跨语言二进制交换等场景中正确使用该接口。读完你将能够:用字节容器 / std::istream / FILE* / 迭代器区间完成 BJData 解码,正确处理 strict 与 allow_exceptions 两种开关,并对齐 BJData 与 UBJSON、MessagePack 等兄弟格式的取舍。
BJData 与 from_bjdata 的定位
BJData 是 UBJSON(Universal Binary JSON,Draft 12) 的派生与改进格式,由 NeuroJSON 社区推动。相比母格式,它引入了三项核心能力(详见仓库文档 docs/mkdocs/docs/features/binary_formats/bjdata.md):
- ND-array(多维打包数组)优化容器,用于高效存储同构数值的多维数组;
- 5 个新类型标记:
[u](uint16)、[m](uint32)、[M](uint64)、[h](float16)、[B](byte),从而无歧义地表达常见的二进制数值类型; - 统一采用小端(little-endian)字节序存储数值,替代 UBJSON 的大端序,避免在主流小端平台上做不必要的字节交换。
与 MessagePack、CBOR 等二进制 JSON 变体相比,BJData 与 UBJSON 有一个罕见组合特性:既是二进制,又“准人类可读”。因为数据中的语义元素(类型标记、字符串名称)都是可直接阅读的 ASCII 字符,因此数据不仅紧凑、读写快,还可以用简单的文本工具直接检索与阅读。
from_bjdata 作为该库处理 BJData 的入口,与序列化方向的 to_bjdata 相对应,是 BJData 完整编解码闭环中的“解码半边”。官方文档明确:任意 JSON 值都可转换为 BJData,反过来任意由 to_bjdata 产生的 BJData 流也都能被 from_bjdata 成功解析(映射是完整的)。
函数签名与模板语义
函数定义于 include/nlohmann/json.hpp,是 basic_json 的静态成员,共两个重载:
// (1) 单输入源
template<typename InputType>
static basic_json from_bjdata(InputType&& i,
const bool strict = true,
const bool allow_exceptions = true);
// (2) 迭代器区间 / C++20 ranges(异构 sentinel)
template<typename IteratorType, typename SentinelType = IteratorType>
static basic_json from_bjdata(IteratorType first, SentinelType last,
const bool strict = true,
const bool allow_exceptions = true);
- 重载 (1):从一个“兼容输入”读取数据;
- 重载 (2):从迭代器区间读取;当
IteratorType与SentinelType为不同类型、且可用operator!=比较时,即成为 C++20 ranges 支持(例如std::counted_iterator搭配std::default_sentinel_t)。
模板参数
InputType(重载 1):凡是能转换为输入适配器(input adapter)的类型皆可,官方文档列出的实例包括:
- 一个
std::istream对象; - 一个
FILE*指针; - 一个 C 风格字符数组;
- 一个指向单字节字符、以空字符结尾的字符串的指针;
- 一个容器
obj,其begin(obj)/end(obj)能产生一对合法迭代器(通过 ADL 或成员函数查找,语义与std::begin/std::end兼容)。
IteratorType:兼容的迭代器类型。
SentinelType:默认为 IteratorType;也可以是与 IteratorType 通过 operator!= 可比较的不同类型,典型用途有:
- 为 C++20 ranges 提供的自定义 sentinel;
- 当
IteratorType是std::counted_iterator时使用std::default_sentinel_t。
函数参数
| 参数 | 方向 | 含义 |
|---|---|---|
i |
in | 可转换为输入适配器的 BJData 格式输入 |
first |
in | 指向输入起始位置的迭代器 |
last |
in | 指向输入结束位置的迭代器,或与末尾迭代器经 operator!= 判等的 sentinel |
strict |
in | 是否要求输入被完整消费至 EOF(默认 true) |
allow_exceptions |
in | 解析出错时是否抛出异常(可选,默认 true) |
返回值
返回反序列化得到的 JSON 值。若发生解析错误且 allow_exceptions = false,返回值为 value_t::discarded,可用 is_discarded 检测:
json j = json::from_bjdata(bad_input, true, false);
if (j.is_discarded())
{
// 解析失败,按业务需要降级处理
}
底层调用链:从静态方法到 BJData 解析器
from_bjdata 的实现非常薄,真实工作全部委托给内部解析设施。以重载 (1) 为例(include/nlohmann/json.hpp):
template<typename InputType>
static basic_json from_bjdata(InputType&& i,
const bool strict = true,
const bool allow_exceptions = true)
{
basic_json result;
auto ia = detail::input_adapter(std::forward<InputType>(i));
detail::json_sax_dom_parser<basic_json, decltype(ia)> sdp(result, allow_exceptions);
const bool res = binary_reader<decltype(ia)>(std::move(ia), input_format_t::bjdata)
.sax_parse(input_format_t::bjdata, &sdp, strict);
return res ? result : basic_json(value_t::discarded);
}
其核心链路可拆成四步:
detail::input_adapter(...):把任意的InputType(std::istream、FILE*、字符数组、带begin/end的容器等)统一包装为输入适配器。重载 (2) 则调用detail::input_adapter(std::move(first), std::move(last))将迭代器区间同样包装成适配器,因此两种重载最终走同一套解析逻辑。json_sax_dom_parser<basic_json, decltype(ia)> sdp(result, allow_exceptions):构造 SAX 事件驱动的 DOM 组装器,它把解析器吐出的“开始数组 / 键 / 数值 / 字符串”等事件逐步还原为树形json值,并接收allow_exceptions控制是否抛异常。binary_reader<decltype(ia)>(std::move(ia), input_format_t::bjdata):以格式枚举input_format_t::bjdata构造二进制读取器。该枚举定义于 include/nlohmann/detail/input/input_adapters.hpp:enum class input_format_t { json, cbor, msgpack, ubjson, bson, bjdata };。.sax_parse(input_format_t::bjdata, &sdp, strict):真正的字节级解析。
值得注意的底层事实是:由于 BJData 是 UBJSON 的派生格式,仓库在 include/nlohmann/detail/input/binary_reader.hpp 中让 BJData 与 UBJSON 共用同一个内部解析函数 parse_ubjson_internal(),并以 if (input_format != input_format_t::bjdata) 之类的分支区分两者的差异点。也就是说,源码层面把 BJData 视为“UBJSON 的一个超集变体”来实现,这与格式定义文档的描述完全一致。
解析器中的 BJData 专属差异
在共用的 parse_ubjson_internal 及其辅助函数中,可以观察到 BJData 特有行为的实现证据:
- 额外数值标记:解析分支允许 BJData 使用
u/m/M/h/B等 UBJSON 没有的标记(见 binary_reader.hpp 中 2000–2300 行附近的字符串长度取值与数值读取分支); - 小端字节序:读取整数与浮点时会根据格式决定是否翻转字节序,代码中可见
is_little_endian != (InputIsLittleEndian || format == input_format_t::bjdata)的判定,即 BJData 固定按小端解释数值; - ND-array 识别:
get_ubjson_size_type()在 BJData 下会检查容器 size 标记的最高位(1 << 8)以识别打包数组(packed array / ND-array),一旦识别,会进一步读取维度信息,并在 binary_reader.hpp 附近把 ND-array 编码为 JData 注释数组格式的对象交给 SAX 处理器。
这种“薄封装 + 深度委托”的设计带来的直接好处是:所有解析错误(过早结束、非法字节、字符串读取失败等)都会统一通过 SAX 的 parse_error 回调上报,再由 json_sax_dom_parser 依据 allow_exceptions 决定抛出异常还是静默返回 discarded,错误语义在不同二进制格式(CBOR/MessagePack/UBJSON/BJData/BSON)间保持一致。
异常与异常安全
异常安全
官方文档给出强保证(strong guarantee):若抛出异常,JSON 值不会发生任何改变。
可能抛出的异常
依据文档与源码(异常经由 binary_reader.hpp 中 parse_error::create(...) 各处上报),from_bjdata 可能抛出以下异常,完整异常族说明见 docs/mkdocs/docs/home/exceptions.md:
| 异常 | 触发场景 |
|---|---|
parse_error.110 |
输入过早结束,或当 strict = true 时数据未消费到 EOF(即存在多余尾部字节) |
parse_error.112 |
发生解析错误(例如遇到非法的类型标记字节) |
parse_error.113 |
字符串无法被成功解析(长度读取失败、字符串内容异常等) |
out_of_range.408 |
优化容器或 n 维数组的 size 无法用 std::size_t 表示(尺寸溢出) |
在 tests/src/unit-bjdata.cpp 中可以找到对应的验证用例,例如截断的 float16 输入会命中 110:
CHECK_THROWS_WITH_AS(_ = json::from_bjdata(vec0),
"[json.exception.parse_error.110] parse error at byte 2: ... unexpected end of input",
json::parse_error&);
CHECK(json::from_bjdata(vec0, true, false).is_discarded());
同一用例同时展示了:在 allow_exceptions = false 时改为返回 discarded 值而非抛出。
strict 参数的作用
strict 为 true(默认)时,解析器要求输入被完整消费:如果整个输入被解析完却仍未到达 EOF,或读到输入末尾时数据结构尚未闭合,都会触发 parse_error.110。这在需要确保没有多余尾随字节的协议场景(例如“一帧 = 恰好一条消息”)中尤为有用;而当你从大缓冲区中解析第一条消息、允许其后还有别的数据时,可考虑关闭严格模式。
完整示例:字节向量解码
官方为 from_bjdata 提供的可运行示例位于 docs/mkdocs/docs/examples/from_bjdata.cpp:
#include <iostream>
#include <iomanip>
#include <nlohmann/json.hpp>
using json = nlohmann::json;
int main()
{
// create byte vector
std::vector<std::uint8_t> v = {0x7B, 0x69, 0x07, 0x63, 0x6F, 0x6D, 0x70, 0x61,
0x63, 0x74, 0x54, 0x69, 0x06, 0x73, 0x63, 0x68,
0x65, 0x6D, 0x61, 0x69, 0x00, 0x7D
};
// deserialize it with BJData
json j = json::from_bjdata(v);
// print the deserialized JSON value
std::cout << std::setw(2) << j << std::endl;
}
该字节序列内部是一个 BJData 对象:0x7B 与 0x7D 分别是对象容器 {、} 的标记,0x54(T)、0x69+数值等字节则编码了布尔与整数成员。输出为(对应 from_bjdata.output):
{
"compact": true,
"schema": 0
}
编译运行方式(假定在仓库根目录,且工作区已安装 C++11 及以上编译器):
g++ -std=c++11 -I single_include docs/mkdocs/docs/examples/from_bjdata.cpp -o from_bjdata_demo
./from_bjdata_demo
这里 -I single_include 指向聚合头文件 single_include/nlohmann/json.hpp,即头文件库的标准引入方式。
迭代器区间与 istream 变体
from_bjdata 的第一个重载接受任何“兼容输入”,因此在上述示例之外,同样的数据还可以用这些等价的写法读取:
// 用 std::istream 读取(例如 std::ifstream / std::stringstream)
json j1 = json::from_bjdata(iss);
// 用迭代器区间(重载 2)
json j2 = json::from_bjdata(v.begin(), v.end());
// 用 C 风格字符数组
std::uint8_t raw[] = {0x7B, 0x69, 0x07, /* ... */ 0x7D};
json j3 = json::from_bjdata(raw);
注意重载 (2) 的两个可选参数与重载 (1) 完全一致,因此 json::from_bjdata(v.begin(), v.end(), false) 之类“非严格 + 不抛异常”的组合也始终可用。测试代码 tests/src/unit-bjdata.cpp 中大量使用了 json::from_bjdata(result, true, false) 这种形态来做 round-trip 断言。
BJData → JSON 类型映射
反序列化方向的映射由 docs/mkdocs/docs/features/binary_formats/bjdata.md 归纳为下表,from_bjdata 依据它把每个 BJData 标记还原为对应 JSON 值:
| BJData 类型 | JSON 值类型 | 标记 |
|---|---|---|
| no-op | 不产生值,继续读下一个值 | N |
| null | null |
Z |
| false | false |
F |
| true | true |
T |
| float16 | number_float | h |
| float32 | number_float | d |
| float64 | number_float | D |
| uint8 | number_unsigned | U |
| int8 | number_integer | i |
| uint16 | number_unsigned | u |
| int16 | number_integer | I |
| uint32 | number_unsigned | m |
| int32 | number_integer | l |
| uint64 | number_unsigned | M |
| int64 | number_integer | L |
| byte | number_unsigned | B |
| string | string | S |
| char | string | C |
| array | array(支持优化格式) | [ |
| ND-array | object(JData 注释数组格式) | [$.#[. |
| object | object(支持优化格式) | { |
| binary | binary(强类型字节数组) | [$B |
几个值得强调的细节:
- no-op
N:解码时不产生任何 JSON 值,仅表示“跳过,读取下一个值”,这是 UBJSON/BJData 家族为流式场景保留的填充语义; char(C) 还原为 string、byte(B) 还原为 number_unsigned,保证类型无损;- 映射是完整的:任何 BJData 值都能转换为一个 JSON 值,这正是官方“
from_bjdata能解析一切to_bjdata输出”保证的另一半。
ND-array 反序列化与 JData 注释数组格式
BJData 最有特色的能力是 ND-array 打包数组。以二维 uint8 数组 [[1,2],[3,4],[5,6]] 为例,UBJSON 必须写成嵌套优化数组 [ [$U#i2 1 2 [$U#i2 3 4 [$U#i2 5 6 ],而 BJData 可以进一步压缩为一个带维度的扁平流:[$U#[$i#i2 2 3 1 2 3 4 5 6 或 [$U#[i2 i3] 1 2 3 4 5 6。
为了在 JSON 侧保留其类型与维度信息,from_bjdata 在解析这类数据时会把 ND-array 转换成 JData 注释数组格式(annotated array format) 的 JSON 对象。上述二维数组解码后形如:
{
"_ArrayType_": "uint8",
"_ArraySize_": [2,3],
"_ArrayData_": [1,2,3,4,5,6]
}
反之亦然:当 to_bjdata 遇到这种形态的对象时,会自动把它压缩回紧凑的 BJData ND-array。其中,当 "_ArraySize_" 只含一个整数、或含两个整数但其中一个为 1 时,生成的是一维优化数组而非 ND-array。
只有当注释确实描述了一个可打包的数组时才会被转换,这要求同时满足:
"_ArrayType_"必须是uint8、int8、uint16、int16、uint32、int32、uint64、int64、single、double、char、byte之一;"_ArraySize_"的每一项都是非负整数,且它们的乘积可以表示为std::size_t;"_ArrayData_"恰好包含指定数量的元素;"_ArrayData_"的每个元素都属于"_ArrayType_"所声明的数值种类(single/double为浮点数,其余为整数)。
另外注意一个明确的版本能力边界(来自官方文档):当前版本尚不支持自动识别并把“嵌套 JSON 数组”直接转换为 BJData ND-array——识别只发生在反向(_ArrayType_ 注释对象 → ND-array)这一方向。
优化容器与优化类型的限制
容器(数组 / 对象)的优化格式由序列化端两个参数控制(详见 to_bjdata):
use_size:在容器开头写入元素个数并省略结束标记;use_type:进一步检查容器内元素是否同型,是则在开头写入类型标记(必须与use_size = true搭配使用)。
需要提醒:仅开启 use_size 反而可能让表示变大,它的价值在于接收端能立刻得知元素个数,便于预分配与流式处理。
在 BJData 中,紧随 $ 标记(优化容器类型指示)之后的合法类型被严格限制为非零定长类型,即只能是 UiuImlMLhdDCB。可变长类型([{SH)和零长类型(TFN)不允许出现在优化容器中——这一限制的动机是节省空间、保持可读性并降低安全风险。
二进制值(binary)与版本兼容
BJData 为二进制数据定义了专用标记 B(优化数组内使用),因此与 UBJSON 不同,二进制数据在 BJData 中既可以序列化也可以反序列化。需要注意版本差异(相关映射见“BJData → JSON 类型映射”表中 [$B → binary):
- Draft 3 模式(需通过
to_bjdata的version参数显式开启)下,二进制值以强类型字节数组形式编解码; - **Draft 2 模式(默认)**下,JSON 中的二进制值会被当作整数列表存储,这意味着含二进制值的 JSON 往返一次后,对象形态会发生变化。
同时,编码数字浮点方面还有一条与文本 dump() 的差异:若 JSON 数字中存储了 NaN 或 Infinity,to_bjdata/from_bjdata 能按 IEEE 浮点正常往返;而文本 dump() 会将它们序列化为 null。
复杂度与适用前提
- 时间复杂度:线性于输入字节数(Linear in the size of the input),符合二进制格式一次扫描即可还原为 DOM 的特性。
- 版本前提:
from_bjdata自 3.11.0 加入;3.13.0 起重载 (1) 扩展了容器输入支持(可接受仅具备 lvalue-only ADLbegin/end、语义与std::begin/std::end一致的类型),3.13.0 起重载 (2) 扩展为接受异构迭代器 + sentinel 组合(C++20 ranges 支持)。请据此选择匹配的库版本。 - 环境前提:
json.hpp是仅头文件的 C++11 库,聚合版位于 single_include/nlohmann/json.hpp,开箱即用。
测试与质量保障
仓库针对 BJData 的测试主要位于 tests/src/unit-bjdata.cpp:它会针对各种 JSON 值调用 to_bjdata 生成字节流再回读断言等价(形如 CHECK(json::from_bjdata(result) == j)),并同时覆盖 strict = false 与 allow_exceptions = false 的组合、float16 特殊值(-0.0、65504.0 等)、非法字节与截断输入、超大数字溢出等负例。此外 tests/src/unit-32bit.cpp 验证 32 位平台上的尺寸语义,tests/src/fuzzer-parse_bjdata.cpp 提供针对 BJData 解析器的模糊测试入口(配合 tests/Makefile 的 fuzz 目标使用),共同保障了解析器的健壮性。
相关 API 一览
BJData 并非孤岛——from_bjdata 与下面这些接口构成完整的二进制格式矩阵,可按需组合:
- to_bjdata:把 JSON 值序列化为 BJData;
- from_cbor:从 CBOR 输入创建 JSON 值;
- from_msgpack:从 MessagePack 输入创建 JSON 值;
- from_bson:从 BSON 输入创建 JSON 值;
- from_ubjson:从 UBJSON 输入创建 JSON 值。
选择建议:在需要多维数组紧凑打包或二进制仍可人工阅读的场合(如科学计算、仪器数据、配置快照交换),BJData 是这些格式中特性最贴近需求的选项;实际落地时,请结合 to_bjdata 的 use_size/use_type/version 参数与本文的 strict/allow_exceptions 开关,设计出能“一次编解码无损往返”且错误处理可控的数据通路。
atomcodeClaude Code 的开源替代方案。连接任意大模型,编辑代码,运行命令,自动验证 — 全自动执行。用 Rust 构建,极致性能。 | An open-source alternative to Claude Code. Connect any LLM, edit code, run commands, and verify changes — autonomously. Built in Rust for speed. Get StartedRust0627
Hy4-previewHy4 preview 是由腾讯混元团队研发的新一代混合专家(MoE)旗舰模型。模型总参数量 770B,每个 token 激活 49B,主干共包含78层,第一层采用标准 FFN,其余 77 层均为 MoE 结构,每层包含 256 个路由专家与 1 个共享专家,每个 token 激活 top-8 路由专家及共享专家。主干之外原生内置 1 层 MTP(总参数量 10B,激活 0.7B)以支持投机解码。Python00
GLM-5.3GLM-5.3 与 GLM-5.2 使用相同的基座模型——所有提升均来自后训练。与 GLM-5.2 相比,它在复杂编程和长程任务上的表现显著提升。Jinja00
GLM-5.3-FlashGLM-5.3-Flash (320B-A18B),是GLM-5系列的首个原生多模态模型。320B总参数,能力超过GLM-5.2Jinja00
Spark-X2.5-4BSpark-X2.5-4B 旨在让强大的 AI 更实用、更高效、更易获得。在广泛日常任务中表现强劲,涵盖对话、写作、翻译、推理、编码、工具调用以及智能体工作流,并在同等规模的开源模型中取得领先成绩。Spark-X2.5 将面向效率的架构与最高 1M tokens 的原生上下文窗口相结合,并支持 200 多种语言。Python00
Spark-X2.5-1.7BSpark-X2.5-1.7B 旨在让强大的 AI 更加实用、高效且易于获取。这些模型在广泛的日常任务中表现出色,涵盖对话、写作、翻译、推理、编程、工具调用和智能体工作流,并在同等规模的开源模型中取得领先结果。Spark-X2.5 将面向效率的架构与最高 1M tokens 的原生上下文窗口相结合,并支持 200 多种语言。Python00