| FazBrowse GitHub Viewer | Trending | | Home |
| Tools: [Download Repo ZIP] [Original HTTPS Page] |
| Name | Name | Last commit date | ||
|---|---|---|---|---|
¹ Payload passthrough is currently only supported by JSON archive.
Important
The next release will deprecate the RapidJSON-based JSON archive in favor of the new built-in implementation (BitSerializer::Json::JsonArchive, see bitserializer/json_archive.h), which requires no external dependencies. The built-in implementation is about 40% faster and offers the same functionality.
Your help with testing the built-in implementation before the switch would be much appreciated — please report any issues at BitSerializer issues.
| Component | Format | Encoding | Pretty format | Based on |
|---|---|---|---|---|
| json-archive | JSON | UTF-8, UTF-16LE, UTF-16BE, UTF-32LE, UTF-32BE | ✅ | Built-in |
| rapidjson-archive | JSON | UTF-8, UTF-16LE, UTF-16BE, UTF-32LE, UTF-32BE | ✅ | RapidJson |
| pugixml-archive | XML | UTF-8, UTF-16LE, UTF-16BE, UTF-32LE, UTF-32BE | ✅ | PugiXml |
| rapidyaml-archive | YAML | UTF-8 | N/A | RapidYAML |
| csv-archive | CSV | UTF-8, UTF-16LE, UTF-16BE, UTF-32LE, UTF-32BE | N/A | Built-in |
| msgpack-archive | MsgPack | Binary | N/A | Built-in |
¹ Versions of the RapidYaml base library less than v0.11.1 does not support Clang-cl(Windows).
² Versions of the RapidYaml base library less than v0.7.1 may be unstable on ARM architecture.
Let's get started with a traditional "Hello world!" example that demonstrates BitSerializer's serialization features, such as validation (e.g., email and phone number formats), post-load value refinement (trimming whitespace, case conversion, fallbacks), handling optional fields, and converting between formats (JSON to CSV). The example highlights the flexibility of the library in handling different data types, including std::chrono, Unicode strings, and data integrity through required/optional constraints.
#include <iostream>
#include "bitserializer/bit_serializer.h"
#include "bitserializer/rapidjson_archive.h"
#include "bitserializer/csv_archive.h"
#include "bitserializer/types/std/vector.h"
#include "bitserializer/types/std/chrono.h"
using namespace BitSerializer;
using JsonArchive = BitSerializer::Json::RapidJson::JsonArchive;
using CsvArchive = BitSerializer::Csv::CsvArchive;
struct CUser
{
// Mandatory fields
uint64_t Id = 0;
std::u16string Name;
std::chrono::system_clock::time_point Birthday;
std::string Email;
// Optional fields (maybe absent or `null` in the source JSON)
std::string PhoneNumber;
std::u32string NickName;
std::string Language;
template <class TArchive>
void Serialize(TArchive& archive)
{
archive << KeyValue("Id", Id, Required());
// Using the `Required()` validator with a custom error message (can be ID of localization string)
archive << KeyValue("Birthday", Birthday, Required("Birthday is required"));
archive << KeyValue("Name", Name, Required(), Validate::MaxSize(32));
archive << KeyValue("Email", Email, Required(), Refine::TrimWhitespace(), Validate::Email());
// Optional field (should be empty or contain a valid phone number)
archive << KeyValue("PhoneNumber", PhoneNumber, Refine::TrimWhitespace(), Validate::PhoneNumber());
archive << KeyValue("NickName", NickName);
// Use fallback value "en" if missing data
archive << KeyValue("Language", Language, Refine::ToLowerCase(), Fallback("en"));
}
};
int main() // NOLINT(bugprone-exception-escape)
{
const char* sourceJson = R"([
{ "Id": 1, "Birthday": "1998-05-15T00:00:00Z", "Name": "John Doe", "Email": "john.doe@example.com", "PhoneNumber": "+(123) 4567890", "NickName": "JD" },
{ "Id": 2, "Birthday": "1993-08-20T00:00:00Z", "Name": "Alice Smith", "Email": "alice.smith@example.com", "PhoneNumber": "+(098) 765-43-21", "NickName": "Ali" },
{ "Id": 3, "Birthday": "2001-03-10T00:00:00Z", "Name": "Ivan Petrov", "Email": "ivan.petrov@example.com", "PhoneNumber": null, "Language": "RU" }
])";
// Load list of users from JSON
std::vector<CUser> users;
BitSerializer::LoadObject<JsonArchive>(users, sourceJson);
// Save to CSV
std::string csv;
BitSerializer::SaveObject<CsvArchive>(users, csv);
std::cout << csv << std::endl;
return EXIT_SUCCESS;
}Example output:
Id,Birthday,Name,Email,PhoneNumber,NickName,Language 1,1998-05-15T00:00:00.0000000Z,John Doe,john.doe@example.com,+(123) 4567890,JD,en 2,1993-08-20T00:00:00.0000000Z,Alice Smith,alice.smith@example.com,+(098) 765-43-21,Ali,en 3,2001-03-10T00:00:00.0000000Z,Ivan Petrov,ivan.petrov@example.com,,,ru
BitSerializer treats missing fields as optional by default, but you can enforce mandatory fields using the Required() validator. This approach eliminates the need for workarounds like using std::optional, simplifying your business logic by avoiding repetitive checks for value existence. The library's robust validation system collects all invalid fields during deserialization, enabling comprehensive error reporting (with localization support if needed). Additionally, BitSerializer ensures type safety by throwing exceptions for type mismatches or overflow errors, such as when deserializing values that exceed the capacity of the target type.
BitSerializer prioritizes reliability and usability, but we understand that performance remains a critical factor for serialization libraries. This chapter provides an overview of the performance characteristics of BitSerializer across various serialization formats, as well as comparative tests with the used third-party libraries.
It is important to note that comparing "Serialization" classes (like BitSerializer) with "Parser" classes (such as RapidJSON, NlohmannJson, PugiXML, or RapidYAML) may not always be entirely fair. These two categories of libraries differ fundamentally in their design and purpose:
It should be noted, that the historical distinction between "DOM parsers" and "stream serializers" is increasingly blurred by modern libraries offering hybrid approaches (e.g. SAX, "on demand").
In this performance analysis, we have benchmarked BitSerializer against the base libraries it relies on (e.g., RapidJSON, PugiXML, and RapidYAML). These libraries are primarily "Parser" classes, and the performance differences observed reflect the inherent trade-offs between DOM-based parsing and streaming serialization.
We understand that comparing "Serialization" classes with "Parser" classes might not always be equitable due to the fundamental differences in their nature (e.g., DOM vs. streaming serialization). However, this comparison provides valuable insights into how BitSerializer performs relative to the libraries it builds upon.
In addition to performance metrics, the size of the serialized output is another important factor to consider when choosing a serialization format. Below is a comparison of the serialized output sizes (in bytes) for the same test model using different formats:
Binary formats like MsgPack produce significantly smaller outputs compared to text-based formats like JSON, XML, or YAML. The CSV format is the most compact among all tested formats, making it an excellent choice for storage and transmission of tabular data.
For most applications, BitSerializer provides the optimal combination of reliability, feature completeness, and performance. Developers working with MsgPack/CSV will see best-in-class speeds, while users needing JSON/XML/YAML benefit from consistent performance with minimal overhead compared to format-specific libraries.
Some archives (JSON, XML and YAML) require third-party libraries, but you can install only the ones which you need. The easiest way is to use one of supported package managers, in this case, third-party libraries will be installed automatically. Please follow instructions for specific archives.
Just add BitSerializer to manifest file (vcpkg.json) in your project:
{
"dependencies": [
{
"name": "bitserializer",
"features": [ "rapidjson-archive", "pugixml-archive", "rapidyaml-archive", "csv-archive", "msgpack-archive" ]
}
]
}Enumerate features which you need, by default all are disabled. Use like as usual in the Cmake.
Alternatively, you can install the library via the command line:
> vcpkg install bitserializer[rapidjson-archive,pugixml-archive,rapidyaml-archive,csv-archive,msgpack-archive]In the square brackets enumerated all available formats, install only which you need.
The recipe of BitSerializer is available on Conan-center, just add BitSerializer to conanfile.txt in your project and enable archives which you need via options (by default all are disabled):
[requires] bitserializer/x.xx [options] bitserializer/*:with_rapidjson=True bitserializer/*:with_pugixml=True bitserializer/*:with_rapidyaml=True bitserializer/*:with_csv=True bitserializer/*:with_msgpack=True
Replace x.xx with the latest available version:
$ git clone https://github.com/PavelKisliak/BitSerializer.git
$ # Enable only archives which you need (by default all are disabled)
$ cmake bitserializer -B bitserializer/build -DBUILD_RAPIDJSON_ARCHIVE=ON -DBUILD_PUGIXML_ARCHIVE=ON -DBUILD_RAPIDYAML_ARCHIVE=ON -DBUILD_CSV_ARCHIVE=ON -DBUILD_MSGPACK_ARCHIVE=ON
$ sudo cmake --build bitserializer/build --config Debug --target install
$ sudo cmake --build bitserializer/build --config Release --target installBy default, will be built a static library, add the CMake parameter -DBUILD_SHARED_LIBS=ON to build shared. You will also need to install dev-packages of base libraries (CSV and MsgPack archives do not require any dependencies), currently available only rapidjson-dev and libpugixml-dev, the RapidYaml library needs to be compiled manually.
Important
Make sure your application and library are compiled with the same options (C++ standard, optimization flags, runtime type, etc.) to avoid binary incompatibility issues.
find_package(bitserializer CONFIG REQUIRED)
# Link only archives which you need
target_link_libraries(${PROJECT_NAME} PRIVATE
BitSerializer::rapidjson-archive
BitSerializer::pugixml-archive
BitSerializer::rapidyaml-archive
BitSerializer::csv-archive
BitSerializer::msgpack-archive
)BitSerializer provides comprehensive Unicode support by enabling serialization of any std::basic_string type (e.g., std::u8string, std::u16string, std::u32string) while automatically handling transcoding to the target output format. You can also use any string type as keys, but keep in mind that transcoding incurs additional processing overhead. For optimal performance, prefer UTF-8 strings, as they are natively supported by all archives and minimize transcoding costs.
The example below demonstrates how BitSerializer seamlessly handles different string types and encodings:
class TestUnicodeClass
{
public:
template <class TArchive>
void Serialize(TArchive& archive)
{
// Serialize a UTF-8 string with key in UTF-16
archive << KeyValue(u"Utf16Key", mUtf8StringValue);
// Serialize a UTF-16 string with key in UTF-32
archive << KeyValue(U"Utf32Key", mUtf16StringValue);
// Serialize a UTF-32 string with key in UTF-8
archive << KeyValue(u8"Utf8Key", mUtf32StringValue);
};
private:
std::string mUtf8StringValue; // UTF-8 encoded string
std::u16string mUtf16StringValue; // UTF-16 encoded string
std::u32string mUtf32StringValue; // UTF-32 encoded string
};This flexibility allows you to work with various Unicode encodings without worrying about manual transcoding. However, for best results, use UTF-8 consistently unless your application specifically requires other encodings.
There are two ways to serialize a class:
Below example demonstrates how to implement internal serialization method:
#include "bitserializer/bit_serializer.h"
#include "bitserializer/rapidjson_archive.h"
using JsonArchive = BitSerializer::Json::RapidJson::JsonArchive;
class TestSimpleClass
{
public:
TestSimpleClass()
: testBool(true)
, testString(L"Hello world!")
{
for (size_t i = 0; i < 3; i++)
{
for (size_t k = 0; k < 2; k++) {
testTwoDimensionArray[i][k] = i * 10 + k;
}
}
}
template <class TArchive>
void Serialize(TArchive& archive)
{
using namespace BitSerializer;
archive << KeyValue("TestBool", testBool);
archive << KeyValue("TestString", testString);
archive << KeyValue("TestTwoDimensionArray", testTwoDimensionArray);
};
private:
bool testBool;
std::wstring testString;
size_t testTwoDimensionArray[3][2];
};
int main()
{
auto simpleObj = TestSimpleClass();
auto result = BitSerializer::SaveObject<JsonArchive>(simpleObj);
return 0;
}Returns result
{
"TestBool": true,
"TestString": "Hello world!",
"TestTwoDimensionArray": [
[0, 1],
[10, 11],
[20, 21]
]
}For serializing a named object please use helper class KeyValue which takes key and value as constructor arguments. Usually the type of key is UTF-8 string, but you are free to use any other convertible type (std::u16string, std::u32string or any numeric types). For example, MsgPack archive has native support for numbers as keys, they will be converted to string when use with another archives. For get maximum performance, better to avoid any conversions.
Tip
For multi-format code, consider using PropertyValue instead of KeyValue if you want scalar fields to be serialized as XML attributes automatically. See Serializing to multiple formats for details.
To serialize the base class, use the helper method BaseObject(), like as in the next example.
template <class TArchive>
void Serialize(TArchive& archive)
{
archive << BaseObject<MyBaseClass>(*this);
archive << KeyValue("TestInt", TestInt);
};Note
Version 0.75 and earlier support serialization of the base class only via the internal Serialize() method.
As alternative for internal Serialize() method also exists approach with defining global functions, it will be useful in next cases:
Note
Internal Serialize() method has higher priority than global one (in v0.75 was a priority for the global function).
You need to implement SerializeObject() in the same namespace as the serializing class, or in BitSerializer:
class TestThirdPartyClass
{
public:
TestThirdPartyClass(int x, int y) noexcept
: x(x), y(y)
{ }
// Example of public property
int x;
// Example of property that is only accessible via a getter/setter
[[nodiscard]] int GetY() const noexcept { return y; }
void SetY(const int inY) noexcept { this->y = inY; }
private:
int y;
};
// Serializes TestThirdPartyClass.
template<typename TArchive>
void SerializeObject(TArchive& archive, TestThirdPartyClass& testThirdPartyClass)
{
// Serialize public property
archive << KeyValue("x", testThirdPartyClass.x);
// Serialize private property
if constexpr (TArchive::IsLoading())
{
int y = 0;
archive << KeyValue("y", y);
testThirdPartyClass.SetY(y);
}
else
{
const int y = testThirdPartyClass.GetY();
archive << KeyValue("y", y);
}
}In this chapter described how to serialize your own class that represent a list of values (similar to std::vector). For this purpose, need to implement a global function SerializeArray() in the same namespace as the serializing class, or in BitSerializer.
Additionally, BitSerializer wants to know the number of elements in the list. This is optional for a text archives like JSON, but mandatory for a binary archive like MsgPack since it stores the size prior the array elements. The size of list can be obtained via one of the following ways:
So, in case if your class has a different signature for the size getter than size(), then you need to implement it as a global function.
Please take a look at the following example:
// Some custom array type
template <typename T>
class CMyArray
{
public:
CMyArray() = default;
CMyArray(std::initializer_list<T> initList)
: mArray(initList)
{ }
[[nodiscard]] size_t GetSize() const noexcept { return mArray.size(); }
void Resize(size_t newSize) { mArray.resize(newSize); }
[[nodiscard]] const T& At(size_t index) const { return mArray.at(index); }
[[nodiscard]] T& At(size_t index) { return mArray.at(index); }
T& PushBack(T&& value) { return mArray.emplace_back(std::forward<T>(value)); }
private:
std::vector<T> mArray;
};
// Returns the size of the CMyArray.
template <class T>
size_t size(const CMyArray<T>& cont) noexcept { return cont.GetSize(); }
// Serializes CMyArray.
template <class TArchive, class TValue>
void SerializeArray(TArchive& arrayScope, CMyArray<TValue>& cont)
{
if constexpr (TArchive::IsLoading())
{
// Resize container when approximate size is known
if (const auto estimatedSize = arrayScope.GetEstimatedSize(); estimatedSize != 0 && cont.GetSize() < estimatedSize) {
cont.Resize(estimatedSize);
}
// Load
size_t loadedItems = 0;
for (; !arrayScope.IsEnd(); ++loadedItems)
{
TValue& value = (loadedItems < cont.GetSize()) ? cont.At(loadedItems) : cont.PushBack({});
Serialize(arrayScope, value);
}
// Resize container for case when loaded items less than there are or were estimated
cont.Resize(loadedItems);
}
else
{
for (size_t i = 0; i < cont.GetSize(); ++i)
{
Serialize(arrayScope, cont.At(i));
}
}
}Additional recommendations:
Most frameworks/engines have their own implementation of the string type, and most likely you will want to add support for serializing these types. BitSerializer allows you to do this in a simple and efficient way by using std::basic_string_view<> as an intermediate type (supported any char type).
Let's imagine that you would like to implement serialization of your own std::string alternative, which is called CMyString. For this purpose you would need two global functions in the same namespace as the serializing class, or in BitSerializer:
template <class TArchive, typename TKey>
bool Serialize(TArchive& archive, TKey&& key, CMyString& value);
template <class TArchive>
bool Serialize(TArchive& archive, CMyString& value);These two functions are necessary for serialization any type with and without key into the output archive. For example, object in the JSON format, has named properties, but JSON-array can contain only values.
Additionally, you will need to implement string conversion methods (internal or global), please read more about (convert sub-module). They will add support for using string types as keys, for example it will allow serialization of std::map<CMyString, int> where CMyString is used as a key.
This all looks a bit more complicated than serializing an object, but the code is pretty simple, please have a look at the example below:
// Some custom string type
class CMyString
{
public:
CMyString() = default;
CMyString(const char* str) : mString(str) { }
bool operator<(const CMyString& rhs) const { return this->mString < rhs.mString; }
const char* data() const noexcept { return mString.data(); }
size_t size() const noexcept { return mString.size(); }
// Required methods for conversion from/to std::string (can be implemented as external functions)
std::string ToString() const { return mString; }
void FromString(std::string_view str) { mString = str; }
private:
std::string mString;
};
// Serializes CMyString with key
template <class TArchive, typename TKey>
bool Serialize(TArchive& archive, TKey&& key, CMyString& value)
{
if constexpr (TArchive::IsLoading())
{
std::string_view stringView;
if (Detail::SerializeString(archive, std::forward<TKey>(key), stringView))
{
value.FromString(stringView);
return true;
}
}
else
{
std::string_view stringView(value.data(), value.size());
return Detail::SerializeString(archive, std::forward<TKey>(key), stringView);
}
return false;
}
// Serializes CMyString without key
template <class TArchive>
bool Serialize(TArchive& archive, CMyString& value)
{
if constexpr (TArchive::IsLoading())
{
std::string_view stringView;
if (Detail::SerializeString(archive, stringView))
{
value.FromString(stringView);
return true;
}
return false;
}
else
{
std::string_view stringView(value.data(), value.size());
return Detail::SerializeString(archive, stringView);
}
}
int main()
{
// Save list of custom strings to JSON
std::vector<CMyString> srcStrList = { "Red", "Green", "Blue" };
std::string jsonResult;
SerializationOptions serializationOptions;
serializationOptions.formatOptions.enableFormat = true;
BitSerializer::SaveObject<JsonArchive>(srcStrList, jsonResult, serializationOptions);
std::cout << "Saved JSON: " << jsonResult << std::endl;
// Load JSON-object to std::map based on custom strings
std::map<CMyString, CMyString> mapResult;
const std::string srcJson = R"({ "Background": "Blue", "PenColor": "White", "PenSize": "3", "PenOpacity": "50" })";
BitSerializer::LoadObject<JsonArchive>(mapResult, srcJson);
std::cout << std::endl << "Loaded map: " << std::endl;
for (const auto& val : mapResult)
{
std::cout << "\t" << val.first.ToString() << ": " << val.second.ToString() << std::endl;
}
return 0;
}Enum types can be serialized as integers or as strings, as you prefer. By default, they serializing as strings, to serialize as integers, use the EnumAsBin wrapper:
archive << KeyValue("EnumValue", EnumAsBin(enumValue));To be able to serialize enum types as string, you need to register a map with string equivalents in the your HEADER file.
// file HttpMethods.h
#pragma once
#include "bitserializer\convert.h"
enum class HttpMethod {
Delete = 1,
Get = 2,
Head = 3
};
BITSERIALIZER_REGISTER_ENUM(HttpMethod, {
{ HttpMethod::Delete, "delete" },
{ HttpMethod::Get, "get" },
{ HttpMethod::Head, "head" }
})
// Optionally, you can declare stream operators (`<<` and `>>`) for the registered enum type
BITSERIALIZER_DECLARE_ENUM_STREAM_OPS(HttpMethod)Note
In the previous version 0.80, used the REGISTER_ENUM and DECLARE_ENUM_STREAM_OPS macros.
One of the core advantages of BitSerializer is the ability to serialize objects into multiple formats through a unified interface. The following example demonstrates how to save the same object to JSON and XML with minimal code changes:
class CPoint
{
public:
CPoint(int x, int y)
: x(x), y(y)
{ }
template <class TArchive>
void Serialize(TArchive& archive)
{
archive << KeyValue("x", x);
archive << KeyValue("y", y);
}
int x, y;
};
int main()
{
auto testObj = CPoint(100, 200);
const auto jsonResult = BitSerializer::SaveObject<JsonArchive>(testObj);
std::cout << "JSON: " << jsonResult << std::endl;
const auto xmlResult = BitSerializer::SaveObject<XmlArchive>(testObj);
std::cout << "XML: " << xmlResult << std::endl;
return 0;
}Output:
JSON: {"x":100,"y":200}
XML: <?xml version="1.0"?><root><x>100</x><y>200</y></root>
The serialization logic differs only in the template parameter (JsonArchive vs XmlArchive). However, there are two aspects worth addressing for production use.
As shown above, XML output contains an auto-generated root node named "root" when no explicit name is provided. This behavior ensures structural consistency across formats but can be customized:
// Specify explicit root node name for XML
const auto xmlResult = BitSerializer::SaveObject<XmlArchive>(KeyValue("Point", testObj));Output:
<?xml version="1.0"?><Point><x>100</x><y>200</y></Point>By default, all fields are serialized as XML elements. However, XML supports attributes, which can produce more compact and idiomatic output for scalar values. BitSerializer provides three approaches to handle this:
Use if constexpr to apply format-specific logic:
template <class TArchive>
void Serialize(TArchive& archive)
{
if constexpr (TArchive::archive_type == ArchiveType::Xml)
{
archive << AttributeValue("x", x);
archive << AttributeValue("y", y);
}
else
{
archive << KeyValue("x", x);
archive << KeyValue("y", y);
}
}Use PropertyValue for automatic adaptation based on archive type and value convertibility:
template <class TArchive>
void Serialize(TArchive& archive)
{
// Scalar types → XML attribute, JSON key
archive << PropertyValue("x", x);
archive << PropertyValue("y", y);
// Complex types → XML element, JSON key (automatic fallback)
archive << PropertyValue("profile", mProfile);
}Use AttributeValue when targeting XML exclusively:
template <class TArchive>
void Serialize(TArchive& archive)
{
archive << AttributeValue("x", x);
archive << AttributeValue("y", y);
}Note
Note: AttributeValue causes a compile-time error when used with non-XML archives (JSON, YAML, CSV, MsgPack).
| Approach | JSON | XML |
|---|---|---|
| KeyValue | {"x":100,"y":200} | <root><x>100</x><y>200</y></root> |
| AttributeValue | ❌ Compile error | <root x="100" y="200"/> |
| PropertyValue | {"x":100,"y":200} | <root x="100" y="200"/> |
Note
PropertyValue automatically falls back to KeyValue for complex types (objects, arrays) that cannot be represented as XML attributes. This behavior is determined at compile time using BitSerializer::Convert::IsConvertible<TValue, std::string>().
BitSerializer has built-in serialization for all STD containers and most other commonly used types. For add support of required STD type just need to include related header file.
| Types | Header |
|---|---|
| std::basic_string<>, std::pmr::basic_string<> | Part of the basic package |
| std::byte | Part of the basic package |
| std::atomic | #include "bitserializer/types/std/atomic.h" |
| std::array | #include "bitserializer/types/std/array.h" |
| std::vector, std::pmr::vector | #include "bitserializer/types/std/vector.h" |
| std::deque, std::pmr::deque | #include "bitserializer/types/std/deque.h" |
| std::bitset | #include "bitserializer/types/std/bitset.h" |
| std::list, std::pmr::list | #include "bitserializer/types/std/list.h" |
| std::forward_list, std::pmr::forward_list | #include "bitserializer/types/std/forward_list.h" |
| std::queue, std::priority_queue | #include "bitserializer/types/std/queue.h" |
| std::stack | #include "bitserializer/types/std/stack.h" |
| std::set, std::multiset, std::pmr::set, std::pmr::multiset | #include "bitserializer/types/std/set.h" |
| std::unordered_set, std::unordered_multiset, std::pmr::unordered_set, std::pmr::unordered_multiset |
#include "bitserializer/types/std/unordered_set.h" |
| std::map, std::multimap, std::pmr::map, std::pmr::multimap | #include "bitserializer/types/std/map.h" |
| std::unordered_map, std::unordered_multimap, std::pmr::unordered_map, std::pmr::unordered_multimap |
#include "bitserializer/types/std/unordered_map.h" |
| std::valarray | #include "bitserializer/types/std/valarray.h" |
| std::pair | #include "bitserializer/types/std/pair.h" |
| std::tuple | #include "bitserializer/types/std/tuple.h" |
| std::optional | #include "bitserializer/types/std/optional.h" |
| std::variant | #include "bitserializer/types/std/variant.h" |
| std::unique_ptr, std::shared_ptr | #include "bitserializer/types/std/memory.h" |
| std::chrono::time_point, chrono::duration | #include "bitserializer/types/std/chrono.h" |
| std::time_t | #include "bitserializer/types/std/ctime.h" |
| std::filesystem::path | #include "bitserializer/types/std/filesystem.h" |
Few words about serialization smart pointers. There is no any system footprints in output archive, for example empty smart pointer will be serialized as NULL type in JSON or in any other suitable way for other archive types. When an object is loading into an empty smart pointer, it will be created, and vice versa, when the loaded object is NULL or does not exist, the smart pointer will be reset. Polymorphism are not supported you should take care about such types by yourself.
BitSerializer does not add any system information when saving the map, for example serialization to JSON would look like this:
std::map<std::string, int> testMap =
{ { "One", 1 }, { "Two", 2 }, { "Three", 3 }, { "Four", 4 }, { "Five", 5 } };
auto jsonResult = BitSerializer::SaveObject<JsonArchive>(testMap);Returns result
{
"Five": 5,
"Four": 4,
"One": 1,
"Three": 3,
"Two": 2
}Below is a more complex example, where loading a vector of maps from JSON.
[{
"One": 1,
"Three": 3,
"Two": 2
}, {
"Five": 5,
"Four": 4
}]Code:
std::vector<std::map<std::string, int>> testVectorOfMaps;
const std::string inputJson = R"([{"One":1,"Three":3,"Two":2},{"Five":5,"Four":4}])";
BitSerializer::LoadObject<JsonArchive>(testVectorOfMaps, inputJson);Since all of the most well-known text formats (such as JSON) allow only text keys, BitSerializer attempts to convert the map key to a string (except binary formats like MsgPack). Out of the box, the library supports all the fundamental types (e.g. bool, int, float) as well as some of the std ones (filesystem::path, chrono::timepoint, etc), but if you want to use your own type as the key, you need to implement the conversion to a string. There are several options with internal and external functions, see details here. For example, you can implement two internal methods in your type:
class YourCustomKey
{
std::string ToString() const { }
void FromString(std::string_view str)
}The ISO 8601 standard was chosen as the representation for the date, time and duration for text type of archives (JSON, XML, YAML, CSV). The MsgPack archive has its own compact time format. For enable serialization of the std::chrono and time_t, just include these headers:
#include "bitserializer/types/std/chrono.h"
#include "bitserializer/types/std/ctime.h"The following table contains all supported types with examples of string representations:
| Type | Format | Examples | References |
|---|---|---|---|
| std::time_t | YYYY-MM-DDThh:mm:ssZ | 1677-09-21T00:12:44Z 2262-04-11T23:47:16Z |
ISO 8601/UTC |
| chrono::time_point | [±]YYYY-MM-DDThh:mm:ss[.SSS]Z | 1872-01-01T04:55:32.021Z 2262-04-11T23:47:16Z 9999-12-31T23:59:59.999Z +12376-01-20T00:00:00Z -1241-06-23T00:00:00Z |
ISO 8601/UTC |
| chrono::duration | [±]PnWnDTnHnMnS | P125DT55M41S PT10H20.346S P10DT25M P35W5D |
ISO 8601/Duration |
Time point notes:
Duration notes:
Since std::time_t is equal to int64_t, need to use special wrapper CTimeRef, otherwise time will be serialized as number.
template <class TArchive>
void Serialize(TArchive& archive)
{
archive << KeyValue("Time", CTimeRef(timeValue));
}In distributed systems, services often need to process messages while forwarding parts they don't interpret. This occurs in API gateways, event routers, and integration points where your service cares about specific fields but must forward the rest of the structure. BitSerializer efficiently handles these scenarios with its payload passthrough feature.
Note
Currently only one archive (RapidJson) supports data passthrough.
using namespace BitSerializer;
using JsonArchive = BitSerializer::Json::RapidJson::JsonArchive;
// Incoming message structure (from external system)
struct ExternalEvent
{
std::string EventId;
Json::RapidJson::Raw Payload; // Opaque payload
template <typename TArchive>
void Serialize(TArchive& archive)
{
archive << KeyValue("event_id", EventId, Required());
archive << KeyValue("payload", Payload, Required("Must contain valid JSON payload"));
}
};
// Internal routing structure (for our system)
struct RoutingEnvelope
{
std::string RouteId;
Json::RapidJson::Raw Payload; // Pass-through payload
template <typename TArchive>
void Serialize(TArchive& archive)
{
archive << KeyValue("route_id", RouteId);
archive << KeyValue("payload", Payload);
}
};
// Deserialize external event (payload remains raw)
ExternalEvent externalEvent;
BitSerializer::LoadObject<JsonArchive>(externalEvent, incomingMsg);
// Forward payload to internal routing system WITHOUT PROCESSING
RoutingEnvelope envelope{ "route_789", std::move(externalEvent.Payload) };
std::string routedMessage;
BitSerializer::SaveObject<JsonArchive>(envelope, routedMessage);Important
The passthrough data maintains its logical structure but follows standard formatting rules during serialization (whitespace/indentation may differ from original).
This approach minimizes processing overhead for pass-through fields while maintaining container-level validation (e.g., ensuring required fields exist).
The functional style of serialization used in BitSerializer has one advantage over the declarative one - you can write branches depending on the data. To check the current serialization mode, use two static methods - IsLoading() and IsSaving(). As they are «constexpr», you will not have any overhead.
class Foo
{
public:
template <class TArchive>
void Serialize(TArchive& archive)
{
if constexpr (TArchive::IsLoading()) {
// Code which executes in loading mode
}
else {
// Code which executes in saving mode
}
}
}This can be most useful when you need to support multiple versions of a model. By default, library does not add any system fields (like as a version of object), but it's not difficult to add version when you will need:
// Old version of test object (no needs to keep old models, just as example)
struct TestUserV1
{
std::string name; // Deprecated, need to split to first and last name
uint8_t age{};
uint32_t lastOrderId{}; // Deprecated, need to remove
template <class TArchive>
void Serialize(TArchive& archive)
{
archive << KeyValue("name", name, Required());
archive << KeyValue("age", age);
archive << KeyValue("lastOrderId", lastOrderId);
}
};
// Actual model
struct TestUser
{
// Introduce version field
static constexpr int16_t CurrentVersion = 1;
std::string firstName;
std::string lastName;
uint8_t age{};
std::string country;
template <class TArchive>
void Serialize(TArchive& archive)
{
// Load 'version' field if exists
int16_t version = TArchive::IsSaving() ? CurrentVersion : 0;
archive << KeyValue("version", version);
if constexpr (TArchive::IsLoading())
{
if (version == 0)
{
// Import name from old format
std::string name;
archive << KeyValue("name", name, Required());
const auto spacePos = name.find(' ');
firstName = name.substr(0, spacePos);
lastName = spacePos != std::string::npos ? name.substr(spacePos + 1) : "";
}
else
{
archive << KeyValue("firstName", firstName, Required());
archive << KeyValue("lastName", lastName, Required());
}
}
archive << KeyValue("age", age);
archive << KeyValue("country", country);
}
};
int main()
{
// Save old version
std::vector<TestUserV1> oldUsers {
{ "John Smith", 35, 1254 },
{ "Emily Roberts", 27, 4546 },
{ "James Murphy", 32, 10653 }
};
const auto archive = BitSerializer::SaveObject<MsgPackArchive>(oldUsers);
// Loading with import to new version
std::vector<TestUser> newUsers;
BitSerializer::LoadObject<MsgPackArchive>(newUsers, archive);
return 0;
}All archives in the BitSerializer support streams as well as serialization to files. In comparison to serialization to std::string, streams/files also supports UTF encodings. BitSerializer can detect encoding of input stream by BOM (Byte order mark) and via data analysis, but last is only supported by RapidJson, PugiXml and CSV archives. The output encoding and BOM is configurable via SerializationOptions. The following example shows how to save/load to std::stream:
class CPoint
{
public:
CPoint() = default;
CPoint(int x, int y)
: x(x), y(y)
{ }
template <class TArchive>
void Serialize(TArchive& archive)
{
archive << KeyValue("x", x);
archive << KeyValue("y", y);
}
int x = 0, y = 0;
};
int main()
{
auto testObj = CPoint(100, 200);
SerializationOptions serializationOptions;
serializationOptions.streamOptions.encoding = Convert::Utf::UtfType::Utf8;
serializationOptions.streamOptions.writeBom = false;
// Save to string stream
std::stringstream outputStream;
BitSerializer::SaveObject<JsonArchive>(testObj, outputStream, serializationOptions);
std::cout << outputStream.str() << std::endl;
// Load from string stream
CPoint loadedObj;
BitSerializer::LoadObject<JsonArchive>(loadedObj, outputStream);
assert(loadedObj.x == testObj.x && loadedObj.y == testObj.y);
return 0;
}For save/load to files, BitSerializer provides the following functions (which are just wrappers of serialization methods to streams):
template <typename TArchive, typename T, typename TString>
BitSerializer::SaveObjectToFile<TArchive>(T&& object, TString&& path, const SerializationOptions& serializationOptions = DefaultOptions, bool overwrite = false);
template <typename TArchive, typename T, typename TString>
BitSerializer::LoadObjectFromFile<TArchive>(T&& object, TString&& path, const SerializationOptions& serializationOptions = DefaultOptions);Note
Note that the stream implementation must support the seekg() operation to load fields non-linearly.
Note
Built-in archives (JSON, CSV, MsgPack) deserialize streams incrementally in chunks with bounded memory usage, so they can handle files much larger than available RAM. Third-party archives (RapidJSON, PugiXML, RapidYAML) are DOM-based and keep the whole document in memory.
First, let's list what are considered as errors and will throw exception:
By default, any missed field in the input format (e.g. JSON) is not treated as an error, you can specify a default value using the Fallback() refiner or add the Required() validator if the field is mandatory.
Note
In the previous version 0.80, loading a null (e.g. "myValue": null) value into an object or array (e.g. std::optional<CMyClass>) would throw an exception with error code MismatchedTypes (all archives except MsgPack and CSV).
You can handle std::exception just for log errors, but if you need to provide more detailed information to the user, you may need to handle the following exceptions:
try
{
int testInt;
BitSerializer::LoadObject<JsonArchive>(testInt, L"10 ?");
}
catch (const BitSerializer::ParsingException& ex)
{
// Parsing error: Malformed token
std::string message = ex.what();
size_t line = ex.Line;
size_t offset = ex.Offset;
}
catch (const BitSerializer::ValidationException& ex)
{
// Handle validation errors
const auto& validationErrors = ex.GetValidationErrors();
}
catch (const std::exception& ex)
{
// Handle any other errors
std::string message = ex.what();
}BitSerializer provides a comprehensive and extensible validation system that enables you to enforce data integrity constraints during deserialization. The library supports both built-in validators and custom validation logic, with all validation errors collected and reported in a single ValidationException.
Validators are applied directly within the serialization interface using a fluent syntax:
archive << KeyValue("testFloat", testFloat, Required(), Validate::Range(-1.0f, 1.0f));
// Or with PropertyValue for multi-format attribute support:
archive << PropertyValue("testFloat", testFloat, Required(), Validate::Range(-1.0f, 1.0f));All validation errors encountered during deserialization are aggregated and thrown as a ValidationException upon completion, enabling comprehensive error reporting rather than failing on the first constraint violation.
By default, there is no limit on the number of validation errors collected. This behavior can be configured via the maxValidationErrors parameter in SerializationOptions. The validation error map contains JSON Pointer paths (RFC 6901) as keys, with each path mapping to a list of error messages for that field.
try {
BitSerializer::LoadObject<JsonArchive>(user, json);
}
catch (BitSerializer::ValidationException& ex) {
const auto& validationErrors = ex.GetValidationErrors();
// Process errors...
}All validators support customizable error messages, which is particularly useful for localization scenarios:
archive << KeyValue("Age", mAge,
Required("Age is required"),
Validate::Range(0, 150, "Age must be between 0 and 150 (inclusive)"));Error messages can contain either descriptive text or localization string identifiers.
All validators are declared in the BitSerializer::Validate namespace (except Required, which is also available in the BitSerializer namespace).
| Signature | Description |
|---|---|
| Required(errorMessage = nullptr) | Ensures the field is present in the source data |
| GreaterThan(threshold, errorMessage = nullptr) | Validates value > threshold |
| GreaterThanOrEqual(threshold, errorMessage = nullptr) | Validates value >= threshold |
| LessThan(threshold, errorMessage = nullptr) | Validates value < threshold |
| LessThanOrEqual(threshold, errorMessage = nullptr) | Validates value <= threshold |
| Range(min, max, errorMessage = nullptr) | Validates min <= value <= max |
| MultipleOf(divisor, errorMessage = nullptr) | Validates that value is a multiple of divisor (supports integer and floating-point types with epsilon-based comparison) |
| MinSize(minSize, errorMessage = nullptr) | Ensures containers or strings meet minimum size requirements |
| MaxSize(maxSize, errorMessage = nullptr) | Ensures containers or strings do not exceed maximum size |
| Email(errorMessage = nullptr) | Validates email format per RFC standards (excluding quoted parts, comments, SMTPUTF8, and IP domains)) |
| PhoneNumber(minDigits = 7, maxDigits = 15, isPlusRequired = true, errorMessage = nullptr) | Validates international phone number format with configurable digit constraints |
| Uuid(errorMessage = nullptr) | Validates UUID format according to RFC 4122/RFC 9562 |
Note
Comparison validators (GreaterThan, LessThan, etc.) support any type that implements the corresponding comparison operators, including std::chrono time points and durations.
For domain-specific validation logic, you can provide custom lambda validators:
archive << KeyValue("NickName", mNickName, [](const std::string& value, bool isLoaded) -> std::optional<std::string> {
if (!isLoaded || value.find_first_of(' ') == std::string::npos) {
return std::nullopt;
}
return "Nickname must not contain spaces";
});Custom validators receive the deserialized value and a boolean indicating whether the field was present in the source data, returning an error message when validation fails.
using namespace BitSerializer;
using JsonArchive = BitSerializer::Json::RapidJson::JsonArchive;
class UserModel
{
public:
template <class TArchive>
void Serialize(TArchive& archive)
{
archive << KeyValue("Id", mId, Required());
archive << KeyValue("Age", mAge, Required("Age is required"), Validate::Range(0, 150, "Age must be between 0 and 150 (inclusive)"));
archive << KeyValue("FirstName", mFirstName, Required(), Validate::MaxSize(16));
archive << KeyValue("LastName", mLastName, Required(), Validate::MaxSize(16));
archive << KeyValue("Email", mEmail, Required(), Validate::Email());
// Custom validation with lambda
archive << KeyValue("NickName", mNickName, [](const std::string& value, bool isLoaded) -> std::optional<std::string>
{
// Loaded string should has text without spaces or should be NULL
if (!isLoaded || value.find_first_of(' ') == std::string::npos) {
return std::nullopt;
}
return "Nickname must not contain spaces";
});
}
private:
uint64_t mId = 0;
uint16_t mAge = 0;
std::string mFirstName;
std::string mLastName;
std::string mEmail;
std::string mNickName;
};
int main()
{
UserModel user;
const char* json = R"({ "Id": 12420, "Age": 500, "FirstName": "John Smith-Cotatonovich", "NickName": "Smith 2000", "Email": "smith 2000@mail.com" })";
try
{
BitSerializer::LoadObject<JsonArchive>(user, json);
}
catch (BitSerializer::ValidationException& ex)
{
const auto& validationErrors = ex.GetValidationErrors();
std::cout << "Validation errors: " << std::endl;
for (const auto& keyErrors : validationErrors)
{
std::cout << "Path: " << keyErrors.first << std::endl;
for (const auto& err : keyErrors.second)
{
std::cout << "\t" << err << std::endl;
}
}
}
catch (std::exception& ex)
{
std::cout << ex.what();
}
return EXIT_SUCCESS;
}Execution output:
Validation errors:
Path: /Age
Age must be between 0 and 150 (inclusive)
Path: /Email
Invalid email format
Path: /FirstName
Size must not exceed 16
Path: /LastName
Value is required
Path: /NickName
Nickname must not contain spaces
The validation system provides flexible constraint checking with efficient error aggregation, suitable for applications requiring robust data integrity without significant performance overhead.
Note
New feature, available since v0.85.
In addition to validators, BitSerializer also has the ability to transform deserialized values using specialized processors called "Refiners". This feature is designed to ensure data quality and consistency by cleaning, normalizing, and providing default values for missing data. Refiners are applied to fields alongside validators using the familiar KeyValue syntax:
archive << KeyValue("Username", mUsername,
Required(),
Refine::TrimWhitespace(),
Refine::ToLowerCase(),
Validate::MaxSize(32));The order of validators and refiners is crucial - they are processed from left to right. Refiners should typically be placed before validators that depend on the refined data:
// ✅ Correct: Trim first, then validate
archive << KeyValue("Email", mEmail,
Required(),
Refine::TrimWhitespace(),
Validate::Email());
// ❌ Incorrect: Validate before trimming
archive << KeyValue("ApiEndpoint", mApiEndpoint,
Required(),
Validate::Email(),
Refine::TrimWhitespace()); // Validation may fail due to trailing whitespaceAvailable refiners:
| Refiner | Description |
|---|---|
| Fallback(defaultValue) | Provides a default value when the field is missing or null |
| TrimWhitespace() | Removes leading and trailing whitespace from strings |
| ToLowerCase() | Converts ASCII letters to lowercase |
| ToUpperCase() | Converts ASCII letters to uppercase |
All refiners are declared in the BitSerializer::Refine namespace, except Fallback which also has alias in the BitSerializer. It's quite easy to write your own refiner or use a lambda function (similar to validators).
BitSerializer performs format-specific validation during compilation, catching serialization errors before runtime by verifying your code against the actual constraints of the target output format. This will help you get immediate feedback, ensuring that your serialized data always conforms to the target format specification.
int testNumber = 12345;
std::string outputData;
// ✅ Correct: Json supports serialization number as root element
BitSerializer::SaveObject<JsonArchive>(testNumber, outputData);
// ❌ Invalid: CSV only supports array of objects, attempting to serialize a number will not compile:
// static_assert failed:
// 'BitSerializer. The archive doesn't support serialize fundamental type without key on this level.'
BitSerializer::SaveObject<CsvArchive>(testNumber, outputData);Each of the supported archives has its own page with details (installation, features, samples, etc.):
Additionally, you may want to use the string conversion submodule.
MIT, Copyright (C) 2018-2026 by Pavel Kisliak, made in Belarus 🇧🇾
| Back | FazBrowse Home | New Git URL |