
很多刚接触Qt的开发者会在Json选型上纠结到底用自带的QJsonDocument还是引入第三方Json库。我的建议很简单——如果项目本身就是Qt技术栈自带的Json支持完全够用而且从Qt 5到Qt 6这套API一直稳定保留。这篇文章不打算抄官方文档里的示例而是按我实际写业务时的顺序复盘怎么解析、怎么构造、怎么落盘、哪些细节最容易出问题最后再聊聊业务对象和Json互转的封装思路。内容适合刚入门Qt、想快速把Json用起来的开发者也适合写了一段时间但总在编码、精度这些地方犯迷糊的朋友。1. 先弄清QJson家族的分工后面代码才不晕1.1 四个核心类各管哪一块Qt对Json的支持主要有四个类QJsonDocument、QJsonObject、QJsonArray、QJsonValue。它们的关系很好理解我打个比方QJsonDocument是整个Json文档的容器好比一个文件夹QJsonObject对应一个大括号{}包裹的键值对区域好比文件夹里的一页表格QJsonArray对应中括号[]包裹的有序列表好比表格上的一列数据而QJsonValue则是单元格里面的值可以是字符串、数字、布尔值、null也可以是嵌套的QJsonObject或QJsonArray。第一次接触的人常犯的错误是纠结于读取Json时到底该先创建QJsonObject还是QJsonDocument。顺序其实很固定任何Json解析第一步都是把原始字节交给QJsonDocument::fromJson()得到一个文档对象然后再从文档里取出顶层对象或顶层数组真正操作数据的阶段才轮到QJsonObject和QJsonArray。反过来当你从零构造一段Json时是先建QJsonObject和QJsonArray拼好数据最后用QJsonDocument包一层再输出成字符串。理解这个解析自上而下、构造自下而上的方向后面基本不会乱。1.2 为什么QJsonValue一个类能吃下所有类型QJsonValue是这套API里的接口人。它内部用type()方法告诉我们当前到底存的是什么同时提供了isObject()、isArray()、isString()、isDouble()、isBool()、isNull()、isUndefined()这一串判断方法。很多业务Bug就出在判断类型这件事上。尤其要注意isNull()和isUndefined()完全是两码事。举个例子如果Json里写的是name: null那么value(name)返回的QJsonValue是Null类型isNull()返回true但如果键根本不存在比如value(age)返回的是UndefinedisUndefined()为trueisNull()反而为false。很多人在取一个可能缺失的字段时习惯用isNull()做判断这会把字段不存在和字段值为null混为一谈后面业务逻辑就容易走错分支。我后面会单独展开讲这一点因为它真的很坑。QJsonValue还有一个容易被忽略的小细节它本身是值语义的类拷贝开销很小。所以在嵌套取值的时候把中间结果直接存成QJsonValue再继续取代码会清爽不少也不需要到处声明临时QJsonObject变量。2. 字符串解析从fromJson到逐层取值的标准流程2.1 错误检查别只检查是否失败还要看offset无论数据来自文件、网络还是用户输入只要拿到字节串统一都用QJsonDocument::fromJson()处理。这个函数接受QByteArray返回一个QJsonDocument出错时会把错误信息填到QJsonParseError里。QByteArray rawData ...; // 可能是文件读出来的也可能是接口返回的 QJsonParseError parseError; QJsonDocument doc QJsonDocument::fromJson(rawData, parseError); if (parseError.error ! QJsonParseError::NoError) { qWarning() Json解析失败: parseError.errorString() 位置: parseError.offset; return; }这里我有一个小习惯除了看errorString()一定会同时把parseError.offset打出来。offset是出错时在原始字符串中的偏移量联调时特别有用。尤其当数据是从服务端返回的时候返回一段好几千行的Json如果只有一句unexpected token根本不知道去哪里找问题有了offset直接到原始文本对应位置看一下多半能立刻发现是引号没配对还是多了一个逗号。还有一点QJsonDocument::fromJson()接收的是QByteArray不是QString。也就是说如果你手里是QString要先通过toUtf8()转成字节而不是直接传入。很多网络接口框架已经把响应体转成了QString这时候别偷懒一定要先转UTF-8再解析。2.2 一层一层取值的代码模板解析成功之后先要判断顶层结构是对象还是数组。实际业务里顶层基本都是对象但偶尔也有接口直接返回数组所以稳妥写法是if (doc.isObject()) { QJsonObject rootObj doc.object(); // 继续用 rootObj 取字段 } else if (doc.isArray()) { QJsonArray rootArray doc.array(); // 当成数组处理 }在对象里取值最核心的方法就是value()。我给的模板是这样的QString name rootObj.value(name).toString(); int age rootObj.value(age).toInt(); bool isVip rootObj.value(isVip).toBool();注意toInt()、toString()这些转换方法的默认行为是尽力转换。比如Json里age是28这个字符串它也能尽量变成整数28如果类型完全不匹配则返回默认值数字返回0字符串返回空串。这个尽力转换在快速开发时很省事但在严格校验的场合反而是隐患。如果你希望严格一点可以先用isDouble()、isString()判断类型再调用转换方法。嵌套的复杂Json才是日常。假设数据结构是这样的{ user: { profile: { nickname: 老张, phone: 13800000000 }, tags: [qt, json] } }取值可以连续调用中途不需要反复创建临时变量QJsonObject rootObj doc.object(); QString nickname rootObj.value(user).toObject() .value(profile).toObject() .value(nickname).toString();这里有个隐藏知识点value(user)返回的是QJsonValue我们要先toObject()得到QJsonObject然后才能继续value()。如果某个中间层不存在后面的toObject()会返回一个空对象再取下一层就得到Undefined最终toString()返回空串不会导致程序崩溃。也就是说Qt这套API在链式取值时天然是安全的不像某些语言里访问null属性直接抛异常。但代价是一旦字段名写错你拿到的不是异常而是默默的空值Bug更难发现。2.3 顶层是数组怎么办顶层是数组的情况常见于批量查询接口比如返回一批设备列表。处理方式就是遍历QJsonArrayQJsonArray arr doc.array(); for (const QJsonValue item : arr) { if (!item.isObject()) { continue; } QJsonObject obj item.toObject(); QString id obj.value(id).toString(); QString name obj.value(name).toString(); // 处理每一行 }这里我建议在循环里加一层isObject()判断。看似多余但实际数据里混入null或纯数字会让程序措手不及。尤其从第三方接口拿数据时数组元素可能是不规则的做好防御式检查能省掉不少线上问题。3. 数据落盘构造Json、序列化与安全写入文件3.1 声明式构造嵌套对象和数组从零构造Json时Qt的QJsonObject支持初始化列表写起来很直观。比如我要构造上面那个用户对象QJsonObject profileObj; profileObj.insert(nickname, 老张); profileObj.insert(phone, 13800000000); QJsonArray tagsArr; tagsArr.append(qt); tagsArr.append(json); QJsonObject userObj; userObj.insert(profile, profileObj); userObj.insert(tags, tagsArr); QJsonObject rootObj; rootObj.insert(user, userObj);QJsonObject和QJsonArray也有带初始化列表的构造方式QJsonObject profileObj{ {nickname, 老张}, {phone, 13800000000} }; QJsonArray tagsArr{qt, json}; QJsonObject userObj{ {profile, profileObj}, {tags, tagsArr} };两种写法等价我平时偏爱初始化列表因为它把嵌套结构直接摊开读代码的时候不用来回找插入语句。但要提醒一点insert同一键会覆盖旧值初始化列表里如果出现两个相同的键也一样后面的生效。这种静默覆盖在排查问题时很容易被忽略。构造完别忘了用QJsonDocument包一层QJsonDocument doc(rootObj);到了这一步数据还在内存里真正要落地还需要序列化。3.2 Compact和Indented两种输出怎么选QJsonDocument::toJson()有两个重载默认参数输出紧凑格式也就是去掉所有多余空格和换行传QJsonDocument::Indented则输出带缩进的格式化内容。QByteArray compactData doc.toJson(QJsonDocument::Compact); QByteArray prettyData doc.toJson(QJsonDocument::Indented);Compact格式适合网络传输、存数据库字段、签名计算体积小Indented格式适合写配置文件、写日志、联调时人工阅读。一个常见场景是程序启动时读取配置文件如果程序发现配置缺失会自动生成一份默认配置这种生成一定要用Indented否则用户打开配置文件看一行好几千字符的Json第一反应就是程序坏了。顺便提一句Qt输出Json默认就是UTF-8编码的QByteArray不含中文时是纯ASCII安全含中文时就是标准的UTF-8字节写文件的时候保持字节原样写入即可不要再做额外的编码转换。3.3 用QSaveFile代替QFile写文件写Json文件看起来用QFile就够了但如果你写过需要定期保存的运行配置一定会遇到程序写到一半崩溃配置文件损坏的情况。Qt提供了一个更稳的类QSaveFile。它先把数据写进同一个目录下的临时文件调用commit()时才真正替换目标文件。如果中途崩溃原始文件还能保留。QSaveFile saveFile(/path/to/config.json); if (!saveFile.open(QIODevice::WriteOnly)) { qWarning() 无法打开文件写入: saveFile.errorString(); return; } saveFile.write(doc.toJson(QJsonDocument::Indented)); if (!saveFile.commit()) { qWarning() 写入配置失败: saveFile.errorString(); }注意QSaveFile虽然名字里有File但用法和QFile几乎一样。唯一要记住的是写完一定要调用commit()直接把saveFile销毁相当于放弃本次写入临时文件会被自动清理。我不止一次看到有人写完忘了commit结果文件始终没更新排查了半天。还有一个细节打开文件时不要加QIODevice::Text标志。加了这个标志Windows平台上Qt会自动做换行符转换把\n变成\r\n。Json字符串里如果包含字面换行会受干扰。直接用默认的WriteOnly二进制模式才能保证字节原样落盘。4. 实际项目中踩过坑这里全列出来4.1 UTF-8中文乱码问题出在源文件而不是Json很多人第一次在Qt里构造带中文的Json解析回来发现乱码第一反应是Json不支持中文。其实Json规范本身就是UTF-8QJsonDocument内部也没问题。真正的问题往往出在源码文件编码上。Windows下MSVC编译时如果源文件是GBK编码且没加对应的编译选项老张这样的字符串字面量会以本地编码塞进可执行文件。到了Qt里QString拿到的是被误解的字节序列再转成UTF-8输出自然就乱了。解决办法主要有三个源文件统一保存为UTF-8并开启/utf-8编译选项MSVC所有中文字符串字面量用QStringLiteral(u8老张)包裹强制按UTF-8处理这种方式在需要配合toUtf8()的场景最省心从外部文件读配置时先确认文件是UTF-8必要时读出来手动去掉BOM。另外一个与UTF-8 BOM相关的真实案例Windows上记事本保存UTF-8文件时有可能加BOM头也就是文件开头多了三字节0xEF 0xBB 0xBF。Qt的fromJson()遇到BOM不会自动跳过直接解析会报错。我的做法是读文件后先判断一下QByteArray raw file.readAll(); if (raw.startsWith(\xEF\xBB\xBF)) { raw.remove(0, 3); }去掉BOM再交给fromJson()问题立刻消失。4.2 数字精度超过2^53的整数请用字符串QJsonValue内部存数字用的是double。这意味着超过2^53的整数会丢失精度。现实里踩得最狠的就是订单号、用户ID、雪花ID这类超过16位的数字。QJsonObject obj; obj.insert(orderId, 210123456789012345678);这样插入再序列化出来你可能会发现约等于210123456789012350000。这个精度丢失是double本身的机制不是Qt的Bug。我吃过这个亏之后总结了一条铁律凡是超过2^53范围内的整数或者可能被客户端当字符串处理的ID字段一律按字符串来存。obj.insert(orderId, QStringLiteral(210123456789012345678));这样不仅精度不会丢很多后端语言里ID本身也是字符串对接起来反而更顺畅。相关的还有金额字段如果涉及小数也别依赖浮点运算存字符串或者整数分单位更靠谱。4.3 isUndefined和isNull不是一回事这个坑我开头提过这里展开讲。业务上常见的一种需求是判断某个字段有没有传。类似if (!rootObj.value(remark).isNull()) { // 用户填了备注 }如果Json里是remark: null这个分支不会进入逻辑看着对。但问题在于接口方可能根本不返回remark这个键此时value(remark)返回的是UndefinedisNull()同样为false你的逻辑不会进入。也就是说键值为null和键不存在被混在一起处理了。反过来如果你想判断默认值没传应该用isUndefined()if (rootObj.value(remark).isUndefined()) { // 没传这个字段 }所以我现在的习惯是需要区分没传和传了null的场景一律分别判断isUndefined()和isNull()不需要区分时干脆先统一下业务口径让接口方约定好到底返回不返回这个键免得两边理解不一致。4.4 键顺序和重复键别依赖QJsonObject的遍历顺序QJsonObject的内部存储是按某种规则组织的不是严格的插入顺序。如果你的业务对字段顺序敏感比如要按特定字段顺序拼字符串做签名校验千万别直接用QJsonObject遍历而是自定义一个有序字段列表再按列表取。重复键的问题也容易忽视。解析下面这段Json时a到底取哪个值{ a: 1, a: 2 }Qt的fromJson()在遇到重复键时后面的值会覆盖前面的。大部分第三方接口不会故意这么返回但万一遇到这种脏数据不要想着能拿到两个值只能拿到其中一个。按规范Json里键本就应该是唯一的客户端处理时也没必要为重复键特意设计只要不依赖第一个保留就行。5. 从裸Json到业务对象映射层的设计思路5.1 手写toJson和fromJson最笨但最稳在Qt项目里我见过两种处理Json和业务对象的风格。一种是在使用处散落大量value()和insert()代码一多就混乱另一种是为每个业务类写两个转换函数。以实际项目中的用户信息结构体为例struct UserInfo { QString name; int age 0; QVectorQString tags; bool isVip false; QJsonObject toJson() const { QJsonArray tagArr; for (const QString tag : tags) { tagArr.append(tag); } return QJsonObject{ {name, name}, {age, age}, {tags, tagArr}, {isVip, isVip} }; } void fromJson(const QJsonObject obj) { name obj.value(name).toString(); age obj.value(age).toInt(); isVip obj.value(isVip).toBool(); tags.clear(); QJsonArray tagArr obj.value(tags).toArray(); for (const QJsonValue v : tagArr) { tags.append(v.toString()); } } };这段代码很多人会觉得啰嗦但它的好处是字段的读写规则全部收敛在一个文件里字段改名、加字段、处理兼容性只需要改这里。项目初期字段少时不觉得等几十个字段散落在十几个界面里调用时这种集中式写法能救命。5.2 QVariantMap中转适合快速改改的写法如果你只是临时处理一份配置或者字段不多可以用QVariantMap中转代码写起来短QVariantMap map object.toVariantMap(); map.insert(level, 3); QJsonObject newObject QJsonObject::fromVariantMap(map);toVariantMap()能递归地把嵌套对象、数组转成QVariant反过来也一样。这套写法适合快速改几个字段不适合做完整业务映射因为一旦涉及特定字段的类型转换比如转枚举、转自定义结构体QVariantMap的表达力就不够了。我一般只在调试或做简单配置合并时用它。需要留意的是嵌套层次太多的JsonQVariantMap中转会频繁发生类型转换性能比直接操作QJsonObject略低。对于配置类的小文件可忽略但如果处理的是MB级别的大Json建议直接用QJson家族的类逐层解析。5.3 往返一致性测试比什么都好用给Json映射层写个往返测试是我的保留项目也是最推荐的验证方式。思路很简单构造一个业务对象toJson()变成Json再fromJson()还原成新对象最后比较两个对象的每个字段是否一致。// 测试伪代码 UserInfo original; original.name 老张; original.tags {qt, json}; UserInfo restored; restored.fromJson(original.toJson()); Q_ASSERT(restored.name original.name); Q_ASSERT(restored.tags original.tags);这个测试最大的价值不是测正常数据能不能通而是能暴露出字段漏写、类型不一致、缺省值不匹配这类问题。我在实际项目中用这种方式维护过二十多个结构体的转换逻辑每次接口字段有调整先跑一遍往返测试基本能在合入前拦住大部分低级错误。如果某个结构体里有时间字段、枚举字段更要写进往返测试里用不同的值跑几遍比拍脑袋保证应该没问题靠谱得多。我自己在维护这类映射层时还有一条额外经验接口返回原始Json在解析前后各打一份日志或落一份文件。联调时如果双方对不上直接把dump出来的Json丢给接口方通常一眼就能看出是哪个字段名写错、哪个嵌套层级偏了。配上Qt的Indented格式输出日志可读性也好线上排查问题的效率能提升不少。这几年代码里Json的使用频率越来越高但Qt这套API的设计一直挺稳核心类也没怎么变过。只要把编码、精度、空值判断这几个基本面掌握住再去处理配置文件、接口对接、对象序列化基本不会遇到特别刁钻的问题。如果你正准备在项目里统一Json处理方案我建议就用自己的Qt内置API起步把映射层做薄一点同时把往返测试补上后续就算字段再多心里也有底。