Введение
После разбора JSON в QJsonDocument/QJsonObject обычно требуется преобразовать данные в типизированные C++ структуры или классы для удобной работы с ними в остальном коде — обращение к obj["field"].toString() повсюду в бизнес-логике быстро становится утомительным и подверженным ошибкам в именах полей. В статье разберём паттерны ручной сериализации/десериализации между QJsonObject и пользовательскими структурами, и обсудим, когда стоит рассмотреть автоматизацию через макросы или сторонние библиотеки.
Концепция
Базовый подход — определить для каждой структуры пару статических/свободных функций fromJson()/toJson(), явно перечисляющих соответствие между полями JSON и членами структуры. Это многословно, но абсолютно прозрачно и не требует дополнительных зависимостей. Для проектов с большим числом структур данных существуют библиотеки автоматической (де)сериализации на основе макросов или рефлексии (например, на основе Q_GADGET с QMetaObject, либо сторонние решения вроде QJsonSerializer), уменьшающие объём шаблонного кода ценой добавления зависимости и менее прозрачного поведения «под капотом».
Пример кода
// product.h
#pragma once
#include <QJsonObject>
#include <QJsonArray>
#include <QString>
#include <QVector>
#include <optional>
struct Product
{
int id;
QString name;
double price;
std::optional<QString> description; // поле может отсутствовать в JSON
QVector<QString> tags;
static Product fromJson(const QJsonObject &obj)
{
Product product;
product.id = obj.value("id").toInt();
product.name = obj.value("name").toString();
product.price = obj.value("price").toDouble();
if (obj.contains("description") && !obj.value("description").isNull()) {
product.description = obj.value("description").toString();
}
const QJsonArray tagsArray = obj.value("tags").toArray();
for (const QJsonValue &tag : tagsArray) {
product.tags.append(tag.toString());
}
return product;
}
QJsonObject toJson() const
{
QJsonObject obj;
obj["id"] = id;
obj["name"] = name;
obj["price"] = price;
if (description.has_value()) {
obj["description"] = description.value();
}
QJsonArray tagsArray;
for (const QString &tag : tags) {
tagsArray.append(tag);
}
obj["tags"] = tagsArray;
return obj;
}
};
// Использование: разбор массива продуктов из ответа API
#include "product.h"
#include <QJsonDocument>
#include <QDebug>
QVector<Product> parseProductList(const QByteArray &jsonData)
{
QVector<Product> products;
const QJsonDocument doc = QJsonDocument::fromJson(jsonData);
if (!doc.isArray()) {
qWarning() << "Ожидался JSON-массив";
return products;
}
for (const QJsonValue &value : doc.array()) {
if (value.isObject()) {
products.append(Product::fromJson(value.toObject()));
}
}
return products;
}
void demonstrateSerialization()
{
const QByteArray input = R"([
{"id": 1, "name": "Клавиатура", "price": 49.99, "tags": ["электроника", "офис"]},
{"id": 2, "name": "Мышь", "price": 19.99, "description": "Беспроводная", "tags": []}
])";
const auto products = parseProductList(input);
for (const auto &product : products) {
qDebug() << product.name << "—" << product.price;
}
QJsonDocument doc(products.first().toJson());
qDebug() << doc.toJson(QJsonDocument::Indented);
}
Пояснения к коду
Product::fromJson() явно читает каждое поле из QJsonObject с использованием .value("key"), что безопасно возвращает «невалидное» значение по умолчанию для отсутствующих ключей вместо краша, в отличие от оператора [] доступа без проверки наличия ключа в некоторых других JSON-библиотеках. Поле description обёрнуто в std::optional, что явно отражает в самом типе C++ структуры тот факт, что это поле может отсутствовать в исходном JSON — этот случай отдельно проверяется через obj.contains().
toJson() выполняет обратное преобразование, симметрично собирая QJsonObject из полей структуры — наличие обоих методов в одном месте (рядом со структурой) облегчает поддержку контракта при изменении полей: добавление нового поля в структуру сразу видно как недостающее в обоих методах сериализации.
Подводные камни
.toInt()/.toString()и аналогичные методыQJsonValueне сообщают об ошибке, если тип значения не совпадает с ожидаемым — для отсутствующего или ошибочно типизированного значения они тихо возвращают значение по умолчанию (0, пустую строку), что может маскировать реальную проблему во входных данных, выдавая её за валидное, но «пустое» значение.- Дублирование списка полей между
fromJson()иtoJson()и самим объявлением структуры — три места, которые нужно обновлять синхронно при изменении состава полей; забытое обновление одного из методов после добавления нового поля в структуру — частый источник несимметричных багов сериализации. - Отсутствие валидации обязательности полей при десериализации. Метод
fromJson()в примере не проверяет, что критичные поля (id,name) действительно присутствовали в исходном JSON — для входных данных, где это важно гарантировать, стоит явно проверятьobj.contains()для каждого обязательного поля и сигнализировать об ошибке (например, черезstd::optional<Product>как возвращаемый тип или явное исключение) при их отсутствии. - Числа с плавающей точкой, представленные в JSON как целые без десятичной точки.
QJsonValue::toDouble()корректно обрабатывает оба варианта, но при обратном round-trip (toJson()→fromJson()) форматирование чисел может незначительно отличаться от исходного представления — это обычно не проблема для большинства приложений, но стоит иметь в виду при тестировании на точное совпадение строкового представления JSON.