做Qt客户端好几年,读写JSON这活儿几乎每三天就遇到一次。平时写配置、接接口、导数据,绕不开这套东西。但说句实话,我在社区里见过不少Qt老手,在JSON上翻车的例子一点不比新手少。所以今天索性把这块好好掰扯一下,从基本API到底层存储机制、从文件读写到编码坑位、从数据校验到性能优化,一次说透。
1. 先弄明白Qt里JSON这套东西的定位
1.1 为什么Qt客户端开发绕不开JSON
移动端、桌面端只要涉及网络通信,服务端返回的数据十有八九是JSON格式。就算不加网络,本地配置用JSON存的也越来越多,相比古老的INI和XML,JSON结构灵活、可读性好、解析库到处都有。Qt从4.8版本开始提供原生JSON支持,一路迭代到现在的QJsonDocument、QJsonObject、QJsonArray、QJsonValue这套组合,已经非常成熟。
很多开发者一上来就搜第三方库,比如rapidjson、nlohmann/json。如果项目没有特殊限制,用Qt自带的JSON模块完全够,而且和信号槽、QVariant的配合是第三方库替代不了的。尤其当你需要把JSON映射到QVariant后直接塞给QML或数据库模块,原生JSON的优势就体现出来了。
1.2 四个核心类的职责和关系
Qt的JSON模块本质是个树形结构模型。顶层是QJsonDocument,往下有QJsonObject(对象,键值对集合)、QJsonArray(数组,有序列表)和QJsonValue(值节点)。具体关系可以这么理解:
- QJsonDocument负责和字符串、二进制数据之间做整体转换,相当于集装箱。
- QJsonObject对应JSON里花括号包裹的部分,底层用的是QHash或者有序映射来存KV对。
- QJsonArray对应方括号包裹的部分,本质上是个QVector。
- QJsonValue是个万能容器,可以装int、double、bool、string、null、QJsonObject、QJsonArray任意一种。
实际写代码时最常犯的认知错误是:觉得QJsonObject就是std::map,可以随便搞。其实Qt为了保证效率,QJsonObject内部是隐式共享的,而且迭代顺序和插入顺序在部分版本里不保证一致。依赖JSON对象键顺序做业务逻辑,早晚会踩坑。
注意:QJsonObject底层是哈希表结构,迭代顺序和插入顺序无关。如果业务上必须保证键顺序,要么换QJsonArray里存键值对,要么自己在键名上做排序,写入前做一次排序处理。
核心的操作习惯应该是:先明确数据的树形结构,再用文档、对象、数组逐层构建或读取,最后统一从QJsonDocument进出。别零散地到处创建对象,然后手动拼字符串,那样既有转义风险又难维护。
1.3 什么时候用Qt原生JSON,什么时候不建议用
不是说Qt原生JSON无所不能。遇到下面几种情况,就得掂量一下:
- 超大JSON文件(上百MB):QJsonDocument的解析方式是一次性全量载入内存,内存占用和文件大小往往不是线性关系,对象成百上千,膨胀个3到5倍很正常。这种场景下用流式解析库(比如saajson、yyjson)更合适。
- 极致性能场景:Qt原生解析器经过多年优化已经不慢,但和专做极致性能的纯C库比还是有差距。后端高并发服务里不会用Qt,但Qt客户端里处理普通JSON,这个差距根本感知不到。
- 需要保留注释的JSON:标准JSON不允许注释,Qt原生模块也不支持。配置里有带注释的需求,要么用JSONC方案先预处理拆注释,要么直接换YAML。
反过来,更看重代码简洁性、和Qt容器无缝衔接、跨平台一致性,选Qt原生就对了。我个人的选型习惯是:单次JSON数据量在50MB以内,QML需要直接对接、或者要跟QVariant互转,统一用Qt原生。超过这个量级,或者是要在非GUI线程里做高频解析,才考虑单独封装yyjson。
2. 读写JSON的底层逻辑与编码细节
2.1 编码问题是一切乱码的根源
JSON文件本身是文本文件,文本就一定涉及字符编码。Qt这边默认的策略是:QJsonDocument::toJson()输出的编码是UTF-8,而fromJson()解析时也默认输入是UTF-8。听起来很清晰,一旦文件是在Windows记事本里以ANSI(GBK)另存的,问题就来了。
我调试过不知道多少次“解析成功但中文全乱”的bug,最终排查路径几乎都是同一个套路:
- 先看文件字节流到底是什么编码。
- 转成UTF-8字符串再交给QJsonDocument。
读文件的时候不要直接拿QFile::readAll()返回的QByteArray丢给fromJson,除非你确定源头是UTF-8。稳妥的做法是先统一转码:
QByteArray rawData = file.readAll(); // 尝试按UTF-8解析,因为Qt内部严格依赖UTF-8 QJsonParseError err; QJsonDocument doc = QJsonDocument::fromJson(rawData, &err);如果rawData是GBK编码,UTF-8解析可能不报错(因为某些GBK字节序列恰好凑成合法的UTF-8字符),也可能直接报“illegal character”。这时候中文就变成了“锟斤拷”或者“�”这类乱码。
正确的处理应该是用QTextStream,明确指定编码读取整个文件:
QFile file(filePath); if (!file.open(QIODevice::ReadOnly | QIODevice::Text)) { return {}; } QTextStream stream(&file); stream.setCodec("UTF-8"); // 如果是GBK文件,就写 "GBK" QString content = stream.readAll(); QJsonDocument doc = QJsonDocument::fromJson(content.toUtf8(), &err);注意,在Windows平台下,如果用户把JSON文件编码另存成了带BOM的UTF-8,读取的时候还要把开头的BOM标记去掉,否则QJsonDocument直接报“非法字符”。处理办法很简单:
if (content.startsWith(u'\uFEFF')) { content.remove(0, 1); }iOS、Android平台下系统默认都是UTF-8,所以移动端开发时编码坑相对少。Windows桌面端才是重灾区,做跨平台工具类的时候一定得把编码问题封装进去,别让业务代码去关心这些。
2.2 QJsonParseError:每次解析都要带着
写这段代码的初衷,是被线上崩溃数据逼出来的。团队里有人写解析代码不检查错误对象,结果上线后偶发数据格式不对,直接崩。后来我要求了,“凡是调用fromJson,必须带QJsonParseError”。
QJsonParseError parseError; QJsonDocument doc = QJsonDocument::fromJson(content.toUtf8(), &parseError); if (parseError.error != QJsonError::NoError) { qWarning() << "JSON解析失败, offset:" << parseError.offset << ", error:" << parseError.errorString(); return {}; }errorString是英文的,不过信息量足够定位。offset非常有用,它直接告诉你崩在哪个字节位置。实战里排查别人返回的坏JSON,把offset和原字符串一对照,立刻能看到是不是某个字段多逗号、少引号。
如果希望错误信息更直观,可以自己按offset把出错的附近行提取出来打日志:
int line = content.left(parseError.offset).count('\n') + 1; int column = parseError.offset - content.lastIndexOf('\n', parseError.offset - 1); qWarning() << "JSON解析错误,第" << line << "行,第" << column << "列";这个处理对定位第三方接口返回的畸形数据非常有帮助。
2.3 QJsonDocument::fromJson的完整解析流程
完整流程很清晰,但很多人习惯直接一把梭。我建议按下面这套来:
- 读取文件内容到QString(用QTextStream统一处理编码)。
- 检查BOM、去空白。
- 调用QJsonDocument::fromJson。
- 检查parseError,失败就直接返回并记录错误上下文。
- 通过doc.isNull()判断是文档本身无效。记住,空文档和空对象不一样。
- 往下取rootObject,进一步判断业务结构对不对。
第5条很多人忽略。QJsonDocument::fromJson("{}")返回的文档是合法的,root是空对象。fromJson("null")返回的文档root是空值。fromJson("")返回的文档isNull()为true。如果代码里不加区分直接rootObject,后两种都取不到有效的QJsonObject,后续逻辑就全乱了。
我习惯在工具类里封装一个返回结果,带状态码和错误描述,而不是让调用方自己判断各种情况:
struct JsonParseResult { bool ok = false; QJsonDocument doc; QString errorString; int errorOffset = -1; }; JsonParseResult parseJsonFromString(const QString &raw) { JsonParseResult result; QJsonParseError err; result.doc = QJsonDocument::fromJson(raw.toUtf8(), &err); if (err.error != QJsonError::NoError) { result.errorString = err.errorString(); result.errorOffset = err.offset; return result; } result.ok = true; return result; }这样写代码的收益在后续维护阶段格外明显。业务方不需要关心JSON底层细节,只看到ok、errorString,测试也好写。
2.4 写JSON时,别手动拼接字符串
新手最爱干这种事:
QString json = "{"; json += "\"name\":\"" + name + "\","; json += "\"age\":" + QString::number(age); json += "}";今天写这代码的开发者,一周后十有八九会被转义符和引号搞崩溃。名称里如果带了个英文双引号,生成的JSON直接非法。正确做法永远是:构建对象树,最后统一toJson():
QJsonObject person; person["name"] = name; person["age"] = age; QJsonObject root; root["person"] = person; QJsonDocument doc(root); QByteArray jsonBytes = doc.toJson(QJsonDocument::Indented);toJson有两个枚举参数:Indented和Compact。Indented会格式化成带缩进的易读版本,Compact会去掉一切多余空白。本地配置我一般用Indented,方便人工排查;接口传输用Compact,省流量。
还有一个经常被忽略的功能:toJson(QJsonDocument::Compact)生成的字节流不带换行符。如果需要写入文件后保持可读性,再单独加个\n结尾,否则部分Windows文本编辑器会提示“文件不以换行符结尾”,虽然不影响解析,但看着难受。
3. 核心API实操细节:读取与构建的每一个环节
3.1 读取JSON对象的值,QJsonValue的坑位
从QJsonObject里取值,最直接的方式:
QJsonValue v = obj["name"];但QJsonValue默认构造是Undefined类型。访问不存在的key时,obj["name"]会返回Undefined,不是Null。想知道key是否存在,要用contains():
if (obj.contains("age") && obj["age"].isDouble()) { int age = obj["age"].toInt(); }很多老代码里常见的错误写法是:
int age = obj["age"].toInt(); // key不存在时返回0 QString name = obj["name"].toString(); // key不存在时返回空字符串这种写法在参数缺失时悄无声息地返回默认值,业务数据被污染后很难查。处理接口数据时,建议严格按照“先判断key是否存在 -> 再判断类型 -> 最后转换”的顺序。
我见过有人封装了一个模板函数来处理这个:
template<typename T> bool getJsonValue(const QJsonObject &obj, const QString &key, T &out) { auto it = obj.constFind(key); if (it == obj.constEnd() || !it->isDouble()) return false; out = it->toVariant().value<T>(); return true; }使用方式就是:
int age; if (getJsonValue(obj, "age", age)) { // 正常逻辑 } else { // 容错逻辑 }注意,toInt()和toDouble()的行为不一样。QJsonValue内部存数值统一按double存储,所以整数123读出来之后,用isDouble()判断是true。从接口拿到的数字,可能是123、也可能123.0,如果想确认有没有小数点,还得转成字符串再判断,这是题外话,但很实用。
3.2 构建嵌套结构:对象套数组,数组套对象
构建嵌套JSON的时候,大多数人脑子里的树形结构到代码里就成了嵌套的临时变量,代码又臭又长。我自己的习惯是写个可以对照树形结构直接看懂的构建过程:
比如要生成下面这个结构:
{ "uid": 1001, "groups": [ {"name": "管理员", "level": 1}, {"name": "访客", "level": 2} ], "profile": { "avatar": "http://example.com/a.png", "tags": ["qt", "cpp"] } }构建代码:
QJsonObject group1; group1["name"] = "管理员"; group1["level"] = 1; QJsonObject group2; group2["name"] = "访客"; group2["level"] = 2; QJsonArray groups; groups.append(group1); groups.append(group2); QJsonArray tags = {"qt", "cpp"}; QJsonObject profile; profile["avatar"] = "http://example.com/a.png"; profile["tags"] = tags; QJsonObject root; root["uid"] = 1001; root["groups"] = groups; root["profile"] = profile; QJsonDocument doc(root); QByteArray data = doc.toJson(QJsonDocument::Compact);这套代码没法简化成“一行流”,一开始我也试过一次性嵌套构造,长度差不多但是可读性差,后期维护非常痛苦。宁可多写几个局部变量,让每一步都清晰。
有个便捷初始化语法值得一提,从C++11开始QJsonObject、QJsonArray都支持初始化列表:
QJsonObject group1{ {"name", "管理员"}, {"level", 1} };写简单数据时非常方便,但嵌套复杂时还是建议分开。
3.3 从文件到对象,再从对象到文件的全链路封装
不封装的后果就是每个业务模块里都有一坨文件读写代码,编码处理、错误判断各写各的,格式还统一不了。后来我总结出最常用的两个工具函数:
读JSON文件:
QJsonObject loadJsonFile(const QString &filePath, bool *ok = nullptr) { QFile file(filePath); if (!file.open(QIODevice::ReadOnly | QIODevice::Text)) { if (ok) *ok = false; return {}; } QTextStream stream(&file); stream.setCodec("UTF-8"); QString content = stream.readAll(); if (content.startsWith(u'\uFEFF')) { content.remove(0, 1); } QJsonParseError err; QJsonDocument doc = QJsonDocument::fromJson(content.toUtf8(), &err); if (err.error != QJsonError::NoError || doc.isNull()) { if (ok) *ok = false; return {}; } if (ok) *ok = true; return doc.object(); }写JSON文件:
bool saveJsonFile(const QString &filePath, const QJsonObject &obj, bool pretty = true) { QFile file(filePath); if (!file.open(QIODevice::WriteOnly | QIODevice::Text | QIODevice::Truncate)) { return false; } QJsonDocument doc(obj); QByteArray data = pretty ? doc.toJson(QJsonDocument::Indented) : doc.toJson(QJsonDocument::Compact); if (!data.endsWith('\n')) { data.append('\n'); } file.write(data); return true; }提示:这个封装函数还不够完整,实际工程里建议再加QSaveFile来写入,保证“要么写入完整,要么保持原状”,防止写配置一半进程崩溃导致文件损坏。QSaveFile在Qt 6里和QFile接口高度一致:
QSaveFile file(filePath); if (file.open(QIODevice::WriteOnly)) { file.write(data); if (!file.commit()) { return false; } }国内做Windows工具类软件,断电、蓝屏都是现实存在的事,原子写文件真心建议养成习惯。
3.4 遍历QJsonArray的三种方式和效率对比
QJsonArray支持indexed访问,也支持Java风格的迭代器(QJsonArray::const_iterator)。效率上差别不大,但可读性和后续维护性上差异明显:
方式一:普通下标循环
for (int i = 0; i < arr.size(); ++i) { QJsonValue v = arr.at(i); if (v.isObject()) { // do something } }方式二:C++11范围for
for (const auto &v : arr) { if (v.isObject()) { // do something } }方式三:const迭代器
for (auto it = arr.constBegin(); it != arr.constEnd(); ++it) { // 通过 *it 访问 QJsonValue }我推荐用方式二,代码最短且没有索引越界问题。数组内元素类型不一致是常见的事,所以每次拿到QJsonValue后的第一件事永远是isObject()、isArray()、isString()判断。别图省事直接转,类型不符的时候默认值会误导你。
遍历对象常有人问“能不能像std::map那样直接for(auto kv: obj)”,Qt的QJsonObject不直接支持这种语法,但可以用key()和value():
QJsonObject obj = ...; for (const QString &key : obj.keys()) { QJsonValue value = obj.value(key); }keys()会返回所有键名的QStringList,数据量很大时会有一次内存分配。在性能敏感的区域,用const迭代器更省事。
4. QVariant与JSON的互相转化:数据桥接的实用技巧
4.1 为什么QVariant这么重要
QVariant是Qt里又一个大杂烩类型,可以装大多数内建类型。JSON树和QVariant树在结构上惊人相似:对象对应QVariantMap,数组对应QVariantList,数字和bool对应同名类型。Qt为此提供了两个现成的桥接方法:
QVariant v = document.toVariant(); QJsonDocument doc = QJsonDocument::fromVariant(v);我早期觉得这俩方法就是鸡肋,后来做项目时开发效率上明显受益了。比如你从数据库拿到一个QVariantList,想转换成JSON存日志,一行搞定。QML和C++交互时,数据在JSON和QVariant之间来回倒的情况非常多,原生桥接比自己手写递归省事太多。
4.2 什么时候该手动转换,什么时候不该用桥接
桥接虽方便,但有个关键限制:内部的数据类型只按JSON原生类型来映射,不经过你的结构体。什么意思?假如接口回了一个Unix时间戳:
{"timestamp": 1672531200}转换成QVariant后,timestamp就是qlonglong,你如果希望它直接变成QDateTime,靠fromVariant做不到。这时候写一个小扩展:
QVariantMap m = doc.toVariant().toMap(); QDateTime dt = QDateTime::fromSecsSinceEpoch(m["timestamp"].toLongLong());再比如null和空字符串的区别,QVariant里是Invalid还是空QString,桥接可能吞掉一部分信息。为了保留业务语义,复杂的结构转来转去还是建议手动写映射。
调用的经验法则:写日志、通用序列化、前端交互用桥接;核心业务数据结构用明确的字段映射。混用也行,但一定在工具层统一封好,不准业务代码到处裸调。
4.3 自定义类型到JSON:实现自己的toJson/fromJson
每个项目里总有几个核心结构体,比如用户信息、配置项、坐标点。与其在业务代码里到处重复obj["x"] = ...,不如给结构体定义两个方法:
struct AppConfig { QString appName; int windowWidth = 1024; int windowHeight = 768; QStringList recentFiles; QJsonObject toJson() const { QJsonObject obj; obj["appName"] = appName; obj["windowWidth"] = windowWidth; obj["windowHeight"] = windowHeight; obj["recentFiles"] = QJsonArray::fromStringList(recentFiles); return obj; } static AppConfig fromJson(const QJsonObject &obj) { AppConfig cfg; cfg.appName = obj["appName"].toString(); cfg.windowWidth = obj["windowWidth"].toInt(1024); cfg.windowHeight = obj["windowHeight"].toInt(768); for (const auto &v : obj["recentFiles"].toArray()) { cfg.recentFiles.append(v.toString()); } return cfg; } };这样每个结构体自己负责序列化和反序列化,职责明确。多层的就用对象套对象,整个项目维护起来心智负担小很多。
心得:我曾经在项目里用宏来生成toJson/fromJson,一开始很爽,后来发现加了字段忘了改宏,编译期又发现不了,反而增加debug成本。现在更倾向于手写,字段少的时候代码量多不了几行,但可读性和可控性高很多。
4.4 实际业务中处理“格式不固定”的JSON
第三方接口时不时给你返回动态key,比如:
{ "2025-04-01": {"count": 1}, "2025-04-02": {"count": 2} }这种结构用结构体硬映射是没戏的,只能用了QVariantMap或者QJsonObject动态遍历:
QJsonObject dateMap = root["data"].toObject(); for (const QString &dateKey : dateMap.keys()) { int count = dateMap[dateKey].toObject()["count"].toInt(); // 处理每个日期 }遇到这种情况,第一反应不是建类去描述,而是想清楚这数据是从哪来、要拿去干什么。如果是日志分析、图表绘制,直接遍历key是最高效的方案。如果能事先确定key集合,还是建议约束结构,宁可多写校验。
5. 坑位排查实录:我踩过的和你们可能踩的
5.1 QJsonValue的toInt()和toDouble()精度陷阱
场景:服务端返回了一个数字id,很大,比如98234759823749823。你用toInt()拿到后突然变成负值,或者数值变小。原因很简单,QJsonValue内部用double保存数字,超出53位精度直接丢。
解决办法是:超大数在JSON里要么当字符串传输,要么客户端用toVariant().toLongLong()。但toLongLong()也只能覆盖到2^63-1。更精确的做法是直接从QJsonValue里拿到原始字符串再自己解析,不过标准JSON里数字就是数字,一旦被解析成double就回不去了。
所以我在设计接口时都会尽量跟服务端协商:长id用字符串传输。客户端处理也没负担,显示原样,比较就字符串比较。
5.2 中文乱码的真凶不是JSON,是编码链路
有一回遇到同样是UTF-8文件,在Linux上解析正常,在Windows上乱码。最后发现的根源是旧的配置文件其实是在Windows记事本里以“ANSI”另存的,记事本说的“Unicode”其实是UTF-16 LE。所以跨平台读取旧文件时,不要假定文件就是UTF-8。
我在工具函数里加了一个简单判断:如果文件前两个字节是FF FE或者FE FF,就是UTF-16,转成QString时用QString::fromUtf16或者QTextStream的setCodec("UTF-16")。如果前三个字节是EF BB BF,就是UTF-8带BOM。如果没有BOM,最常见的还是UTF-8。
有了这层判断,基本能覆盖99%的配置文件来源。
注意:处理用户手动编辑过的JSON文件时一定要考虑换行符问题。Windows下是\r\n,Linux下是\n。Qt的QTextStream处理text mode时在Windows平台会自动转换,但QFile::readAll不会。解析JSON时换行符不影响fromJson,但拼接日志时会被能看到,影响不大,知道就行。
5.3 解析成功但字段全是空:全是类型没判断
社区里很常见的问题:“我用QJsonDocument::fromJson解析了,不报错,但obj["name"]就是不显示”。不用看代码,大概率是从QJsonValue转到非目标类型了。服务端返回"name": null,你用toString()拿到空串;或者返回"age": "18"(字符串),你用toInt()拿到默认值0。
所以每当你从JSON里拿一个值,都要先反问自己:这字段一定是这个类型吗?有没有可能是null?有没有可能是嵌套对象?
代码的健壮性往往就体现在这些地方。
5.4 隐式共享与生命周期:QJsonValue不要长期保存
QJsonValue有个隐蔽但关键的性质:它持有的是内部共享数据,当底层的QJsonDocument、QJsonObject析构或修改后,悬空的QJsonValue访问未定义行为(但通常不会崩,只是拿到脏数据)。我见过有人把QJsonValue存到容器里,等整个请求响应处理完后才去取,结果数据已经被后续解析冲掉。
正确做法是:用完立刻转成具体类型(QString、int、嵌套对象)再存,不要让QJsonValue离开函数作用域。
这里也解释一个老生常谈的bug:QJsonObject obj2 = obj1;之后修改obj2,obj1也会变吗?答案是:这是隐式共享,obj2和obj1最初共享内部数据,只有发生了“写操作”时才会分离(copy-on-write)。如果你在循环里不断复制同一份大对象并只读访问,性能是不错的;但如果每个副本都要修改,就尽量提前detach()或者构造时就深拷贝。实际开发中这个特性带来的问题不多,但概念上一定要清晰。
5.5 大JSON和多线程:坑比想象的深
QJsonDocument从Qt 5时代开始就标注为reentrant,但不保证同一个实例在多个线程同时读写。如果两个线程同时解析不同JSON且不共享同一个QJsonDocument,这没问题。如果共享同一个,就得加锁或者复制。
大数据量解析,建议扔到QtConcurrent或工作线程里,一次性做完再通过信号槽回到主线程更新UI:
QtConcurrent::run([this, rawData] { QJsonParseError err; QJsonDocument doc = QJsonDocument::fromJson(rawData, &err); emit jsonReady(doc); });需要注意的是,QJsonDocument也可以跨线程通过信号槽传递,Qt的信号槽参数如果是自定义类型要记得用qRegisterMetaType<QJsonDocument>(),官方内建类型一般都已经注册过了。实际用起来问题不大,但如果你在自定义枚举和结构体之间来回传,就得自己注册。
6. 后续还能怎么扩展这套JSON能力
6.1 给工具库加上Schema校验
只判断解析成功还不够,业务上经常需要判断结构完整性。比如读取配置文件时,缺少了某个字段我们可能要回退默认值;接口数据缺了字段可能要整包丢弃。与其在业务里写一坨if判断,我可以把字段校验收拢起来,做成一个JSON Schema校验器。早期项目可以直接上现成库,比如Qt里使用JsonSchema验证器,但项目依赖不复杂时也可以自己按需写检查函数:
bool checkRequiredFields(const QJsonObject &obj, const QStringList &required) { for (const auto &key : required) { if (!obj.contains(key) || obj.value(key).isNull()) { qWarning() << "缺少必要字段:" << key; return false; } } return true; }对用户配置类文件,这个函数已经能挡住大量低级错误。
6.2 QML里直接用JSON数据
Qt客户端最大的便利之一就是QML和C++混编时对JSON的支持。QML可以直接把QString解析后的JS对象赋给ListView的model,省掉手动转Model的步骤:
property var dataModel: ({}) function loadJson(path) { var xhr = new XMLHttpRequest; xhr.open("GET", path, false); xhr.send(); var json = JSON.parse(xhr.responseText); dataModel = json; }这边要注意的是XMLHttpRequest在QML里是异步行,它接收到的数据是字符串,不适合处理超大JSON。如果是复杂业务,建议还是在C++侧解析完成,再整体抛给QML。
6.3 支持JSON Lines(NDJSON)格式
日志系统里很常见JSON Lines:每行都是独立的一个JSON对象。Qt的QJsonDocument不支持一次解析多行,解决办法就是逐行解析:
QFile file(logPath); file.open(QIODevice::ReadOnly | QIODevice::Text); while (!file.atEnd()) { QByteArray line = file.readLine().trimmed(); if (line.isEmpty()) continue; QJsonDocument doc = QJsonDocument::fromJson(line, &err); if (err.error == QJsonError::NoError) { // 处理这一行 } }这种方式对几千行日志很好用,对大文件来说也比一次性整个读入稳。注意Windows下换行符的问题,readLine在Qt里已经处理好了,不用太担心。
7. 一些写在最后的经验
我自己做Qt开发这些年,对JSON操作最大的感悟不是API记了多少,而是所有输入都不可信。凡是外部来的JSON,都要按“解析可能失败、字段可能缺失、类型可能不符、编码可能混乱”四个角度去设计代码。不要依赖服务端文档承诺的类型,不要指望用户配置文件一定完整。
给新人的建议就三条:
第一,统一封装。项目里只保留一个读写JSON的入口单元,谁也别自己裸写文件读取。
第二,错误处理写全。parseError、类型判断、默认值、日志,一个都不能少,上线之后你会发现这些“多余”的代码是救命稻草。
第三,复杂结构优先做小模型。把核心结构体映射成toJson/fromJson方法,不要指望所有JSON都能一把generic转换。
另外,用Qt原生JSON模块做加密压缩存储时,可以先用qCompress压缩再转Base64,再塞进JSON存储,不过这是另一个话题了。
说到底,Qt的JSON模块已经够用、好用,关键是设计上给它把好关。把这些基础功磨利索了,后续无论做桌面工具还是嵌入式HMI,都会轻松很多。