首页
/ JSON for Modern C++ 中的 BJData 反序列化:nlohmann::json::from_bjdata 全面解析与实战

JSON for Modern C++ 中的 BJData 反序列化:nlohmann::json::from_bjdata 全面解析与实战

2026-09-07 21:53:57作者:裴锟轩Denise

from_bjdata 是 nlohmann/json(JSON for Modern C++)提供的静态反序列化接口,负责把遵循 BJData(Binary JData) 规范的二进制字节流还原为 json 值。本文基于官方 API 文档与仓库源码,系统讲解两个重载的签名与参数语义、底层解析实现、异常与错误处理,并结合类型映射、ND-array 注释格式、可运行示例与单元测试,帮助你在消息传输、科学计算数组打包、跨语言二进制交换等场景中正确使用该接口。读完你将能够:用字节容器 / std::istream / FILE* / 迭代器区间完成 BJData 解码,正确处理 strictallow_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):从迭代器区间读取;当 IteratorTypeSentinelType 为不同类型、且可用 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;
  • IteratorTypestd::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);
}

其核心链路可拆成四步:

  1. detail::input_adapter(...):把任意的 InputTypestd::istreamFILE*、字符数组、带 begin/end 的容器等)统一包装为输入适配器。重载 (2) 则调用 detail::input_adapter(std::move(first), std::move(last)) 将迭代器区间同样包装成适配器,因此两种重载最终走同一套解析逻辑。
  2. json_sax_dom_parser<basic_json, decltype(ia)> sdp(result, allow_exceptions):构造 SAX 事件驱动的 DOM 组装器,它把解析器吐出的“开始数组 / 键 / 数值 / 字符串”等事件逐步还原为树形 json 值,并接收 allow_exceptions 控制是否抛异常。
  3. binary_reader<decltype(ia)>(std::move(ia), input_format_t::bjdata):以格式枚举 input_format_t::bjdata 构造二进制读取器。该枚举定义于 include/nlohmann/detail/input/input_adapters.hppenum class input_format_t { json, cbor, msgpack, ubjson, bson, bjdata };
  4. .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.hppparse_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 参数的作用

stricttrue(默认)时,解析器要求输入被完整消费:如果整个输入被解析完却仍未到达 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 对象:0x7B0x7D 分别是对象容器 {} 的标记,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) 还原为 stringbyte(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_" 必须是 uint8int8uint16int16uint32int32uint64int64singledoublecharbyte 之一;
  • "_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_bjdataversion 参数显式开启)下,二进制值以强类型字节数组形式编解码;
  • **Draft 2 模式(默认)**下,JSON 中的二进制值会被当作整数列表存储,这意味着含二进制值的 JSON 往返一次后,对象形态会发生变化。

同时,编码数字浮点方面还有一条与文本 dump() 的差异:若 JSON 数字中存储了 NaN 或 Infinity,to_bjdata/from_bjdata 能按 IEEE 浮点正常往返;而文本 dump() 会将它们序列化为 null

复杂度与适用前提

  • 时间复杂度:线性于输入字节数(Linear in the size of the input),符合二进制格式一次扫描即可还原为 DOM 的特性。
  • 版本前提from_bjdata3.11.0 加入;3.13.0 起重载 (1) 扩展了容器输入支持(可接受仅具备 lvalue-only ADL begin/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 = falseallow_exceptions = false 的组合、float16 特殊值(-0.065504.0 等)、非法字节与截断输入、超大数字溢出等负例。此外 tests/src/unit-32bit.cpp 验证 32 位平台上的尺寸语义,tests/src/fuzzer-parse_bjdata.cpp 提供针对 BJData 解析器的模糊测试入口(配合 tests/Makefile 的 fuzz 目标使用),共同保障了解析器的健壮性。

相关 API 一览

BJData 并非孤岛——from_bjdata 与下面这些接口构成完整的二进制格式矩阵,可按需组合:

选择建议:在需要多维数组紧凑打包二进制仍可人工阅读的场合(如科学计算、仪器数据、配置快照交换),BJData 是这些格式中特性最贴近需求的选项;实际落地时,请结合 to_bjdatause_size/use_type/version 参数与本文的 strict/allow_exceptions 开关,设计出能“一次编解码无损往返”且错误处理可控的数据通路。

登录后查看全文
热门项目推荐
相关项目推荐

项目优选

收起
kernelkernel
deepin linux kernel
C
33
18
ops-transformerops-transformer
本项目是CANN提供的transformer类大模型算子库,实现网络在NPU上加速计算。
C++
1.13 K
2.75 K
pytorchpytorch
作为 Ascend for PyTorch 社区的核心组件,TorchNPU 是昇腾专为 PyTorch 打造的深度学习适配插件,使 PyTorch 框架能够直接调用昇腾 NPU,为开发者提供昇腾 AI 处理器的超强算力。
Python
857
1.35 K
docsdocs
暂无描述
Markdown
897
5.8 K
kernelkernel
openEuler内核是openEuler操作系统的核心,既是系统性能与稳定性的基石,也是连接处理器、设备与服务的桥梁。
C
529
593
ops-nnops-nn
本项目是CANN提供的神经网络类计算算子库,实现网络在NPU上加速计算。
C++
915
1.83 K
jiuwenswarmjiuwenswarm
JiuwenSwarm 是一款基于openJiuwen开发的智能AI Agent,它能够将大语言模型的强大能力,通过你日常使用的各类通讯应用,直接延伸至你的指尖。
Python
3.58 K
1.01 K
ops-mathops-math
本项目是CANN提供的数学类基础计算算子库,实现网络在NPU上加速计算。
C++
1.35 K
1.46 K
cann-learning-hubcann-learning-hub
CANN 学习中心仓,支持在线互动运行、边学边练,提供教程、示例与优化方案,一站式助力昇腾开发者快速上手。
Jupyter Notebook
1.01 K
515
AscendNPU-IRAscendNPU-IR
AscendNPU-IR是基于MLIR(Multi-Level Intermediate Representation)构建的,面向昇腾亲和算子编译时使用的中间表示,提供昇腾完备表达能力,通过编译优化提升昇腾AI处理器计算效率,支持通过生态框架使能昇腾AI处理器与深度调优
C++
547
388