November 03, 2025 by Dennis Oberst | Comments
In this post, we explore different approaches for data serialization, comparing the key well-known formats for structure data. These are also all readily available for your Qt projects. We made the comparisons by testing the QDataStream, XML, JSON, CBOR and Protobuf data serialization formats using a realistic scenario. As a result, differences were found in code, data size, and performance in terms of serialization and de-serialization times between each format. You can use these findings to help make informed decisions when choosing which one to use. You'll find detailed coverage of: To properly test these formats, we use a task management data structure with multiple nesting levels and various Qt types including QUuid, QDateTime or QColor. This created a realistic scenario that challenges each data serialization method with complex, nested data. The next step was to write our Qt Test Benchmark. We generated a TaskManager with 10,000 tasks to provide substantial data for testing our data serialization formats. We declared the QtCoreSerialization and QtProtobuf functions to handle their respective formats: Now let’s go through the test for each format in detail. There are various data serialization formats for structured data, as outlined in the Qt Core Serialization overview. We focused on general-purpose formats suitable for complex data structures, excluding application settings. The QDataStream, XML, JSON, and CBOR formats follow a consistent interface where a serialize function converts the TaskManager into its encoded form, and a deserialize function reconstructs the original object. We validated each round-trip by comparing the result with the input data: QDataStream provides Qt's native binary data serialization format, using streaming operators for type-safe data handling. Any type can be serialized by implementing the input and output operators. For serialization, we defined output streaming operators for each data structure, maintaining consistent field order throughout the hierarchy. #include <QtCore/QDataStream>
QDataStream &operator<<(QDataStream &stream, const TaskHeader &header) {
return stream << header.id << header.name << header.created << header.color;
}
QDataStream &operator<<(QDataStream &stream, const Task &task) {
return stream << task.header << task.description << qint8(task.priority) << task.completed;
}
QDataStream &operator<<(QDataStream &stream, const TaskList &list) {
return stream << list.header << list.tasks;
}
QDataStream &operator<<(QDataStream &stream, const TaskManager &manager) {
return stream << manager.user << manager.version << manager.lists;
}
QByteArray serializeDataStream(const TaskManager &manager)
{
QByteArray data;
QDataStream stream(&data, QIODevice::WriteOnly);
stream.setVersion(QDataStream::Qt_6_10);
stream << manager;
return data;
} For deserialization, we implemented matching input operators that read fields in the exact same sequence to ensure data integrity. Qt provides XML serialization through QXmlStreamWriter and QXmlStreamReader. Unlike QDataStream's Qt-specific binary format, XML uses a widely recognized standard that ensures interoperability across different systems and programming languages. For serialization, we use QXmlStreamWriter to convert our data hierarchy into XML elements and attributes: #include <QtCore/QXmlStreamWriter>
void encodeXmlHeader(QXmlStreamWriter &writer, const TaskHeader &header) {
writer.writeAttribute("id"_L1, header.id.toString(QUuid::WithoutBraces));
writer.writeAttribute("name"_L1, header.name);
writer.writeAttribute("color"_L1, header.color.name());
writer.writeAttribute("created"_L1, header.created.toString(Qt::ISODate));
}
void encodeXmlTask(QXmlStreamWriter &writer, const Task &task) {
writer.writeStartElement("task"_L1);
encodeXmlHeader(writer, task.header);
writer.writeAttribute("priority"_L1, QString::number(qToUnderlying(task.priority)));
writer.writeAttribute("completed"_L1, task.completed ? "true"_L1 : "false"_L1);
writer.writeCharacters(task.description);
writer.writeEndElement();
}
void encodeXmlTaskList(QXmlStreamWriter &writer, const TaskList &list) {
writer.writeStartElement("tasklist"_L1);
encodeXmlHeader(writer, list.header);
for (const auto &task : list.tasks)
encodeXmlTask(writer, task);
writer.writeEndElement();
}
void encodeXmlTaskManager(QXmlStreamWriter &writer, const TaskManager &manager) {
writer.writeStartElement("taskmanager"_L1);
writer.writeAttribute("user"_L1, manager.user);
writer.writeAttribute("version"_L1, manager.version.toString());
for (const auto &list : manager.lists)
encodeXmlTaskList(writer, list);
writer.writeEndElement();
}
QByteArray serializeXml(const TaskManager &manager)
{
QByteArray data;
QXmlStreamWriter writer(&data);
writer.writeStartDocument();
encodeXmlTaskManager(writer, manager);
writer.writeEndDocument();
return data;
} For deserialization, QXmlStreamReader processes the XML document sequentially. We traverse elements using readNextStartElement() and extract data from attributes to reconstruct our object hierarchy: Qt provides JSON serialization through QJsonDocument, QJsonObject, and QJsonArray. JSON offers a human-readable text format that's become the standard for web APIs and configuration files. For serialization, we build a JSON document by converting each data structure to QJsonObject with helper functions for nested types: For deserialization, we parse the JSON document and extract values by key, converting QJsonArray to lists and QJsonObject to nested structures: CBOR (Concise Binary Object Representation) provides a compact binary alternative to JSON. Qt offers native support for it through the QCborValue, QCborMap, and QCborArray classes, as well as the QCborStreamReader and QCborStreamWriter APIs. The serialization process is very similar to JSON — each field of the TaskManager is written as a key–value pair. However, the resulting data is stored in a compact binary form, which makes it more space-efficient and faster to parse: For deserialization, the CBOR data is read back into corresponding Qt types using QCborValue or by manually traversing the CBOR structure. This allows an easy round-trip between JSON and CBOR representations while maintaining the same logical structure: Protobuf uses an interface definition language (IDL) to define data structures in .proto files. The protocol buffer compiler then generates the serialization code, making the implementation concise and type-safe. Unlike the other formats where we manually handle serialization logic, Protobuf automatically generates efficient binary serialization code from the schema definition. We defined our data structure using Protobuf's IDL syntax. The QtProtobufQtCoreTypes and QtProtobufQtGuiTypes modules provide automatic conversions for Qt types like QUuid, QDateTime or QColor, allowing seamless integration with Qt's type system: The task_manager.qpb.h header was generated from our .proto file definition. We implemented a conversion operator in the TaskManager struct to bridge between our native type and the generated proto::TaskManager. After converting our test data to the proto format, the QProtobufSerializer handles the actual binary serialization and deserialization, reducing the complex data transformation to straightforward API calls: We benchmarked each serialization format using a TaskManager containing 10'000 tasks distributed across multiple task lists. The tests were conducted using Qt 6.10.0 on macOS with an ARM64 architecture. Here’s a summary of the data serialization format test results: QDataStream proved to be the fastest format overall, achieving sub-millisecond serialization times and quick deserialization. Its use of simple streaming operators makes it highly efficient within Qt-based applications. However, since it is tightly coupled to Qt, QDataStream is best suited for internal data handling or temporary storage within Qt environments. XML offers a human-readable, text-based structure with strong support for standardized schemas. It remains a flexible choice for systems that require transparency or manual editing. Although the resulting files are relatively large due to its verbose syntax, XML performs efficiently in most practical use cases and is well-suited for configuration and data exchange with legacy systems. JSON remains a widely adopted format thanks to its universal compatibility and simplicity. It is particularly well-suited for web applications and network protocols, where lightweight data exchange is essential. While deserialization is fast, serialization is comparatively slower, and the resulting files are the largest among the tested formats. Despite these drawbacks, JSON’s readability and broad ecosystem support make it a reliable choice for APIs and cross-platform data interchange. CBOR provides a compact binary representation of JSON-like data structures, combining structural familiarity with reduced storage requirements compared to JSON. Although its performance is moderate compared to QDataStream, it offers a good balance between compactness and interoperability. CBOR is especially suitable for network protocols or scenarios where JSON compatibility is desired with improved efficiency. Protobuf achieved the smallest serialized output, thanks to its efficient binary encoding and schema-based structure. It enforces strong typing, making data transmission and storage more predictable and safe. However, the need for code generation and the associated conversion overhead can add complexity to development workflows. Protobuf is particularly advantageous for high-volume data storage or network transmission where bandwidth and space efficiency are critical. Choosing the right data serialization format depends on your specific use case, whether you want to optimize for performance, size, readability, or cross-platform compatibility. Where QDataStream excels in speed within Qt environments, formats like JSON and XML offer broader interoperability. CBOR and Protobuf provide balance between efficiency and structure, with Protobuf delivering the most compact output. By understanding the strengths and trade-offs of each data serialization method, you can make informed decisions to optimize your application for speed, scalability, and maintainability. You can find the benchmark code referenced in this post here.
Remarks on Testing with Complex, Nested Data

How Each Data Serialization Format Was Tested
Consistent Interface for QDataStream, XML, JSON, and CBOR
The QDataStream Test
The XML Test
The JSON Test
The CBOR Test
The Protobuf Test
Results: How the Data Serialization Formats Compare
Format
Serialisation Time
Deserialisation Time
Serialized Size
QDataStream
0.50 ms
1 ms
1127 KB
XML
6.5 ms
7.5 ms
1757 KB
JSON
14 ms
6 ms
2005 KB
CBOR
10 ms
6.7 ms
1691 KB
QtProtobuf
10 ms
7.7 ms
890 KB
How QDataStream Compares
When Is XML a Good Choice?
What Is JSON Best for?
Is CBOR Better than JSON?
What is Protobuf Best for?
Conclusion
.png)


