Userver provides classes for reading, working with, and serializing various data formats. Classes for different formats have an almost identical interface. There are common points for customizing parsers and serializers of custom types.
formats::*::Value
Classes formats::json::Value, formats::bson::Value, yaml_config::YamlConfig and formats::yaml::Value are intended for non-modifying work with formats (in other words, for reading data).
Usage Example:
"key1": 1,
"key2": {"key3":"val"}
})");
const auto key1 = json["key1"].As<int>();
ASSERT_EQ(key1, 1);
const auto key3 = json["key2"]["key3"].As<std::string>();
ASSERT_EQ(key3, "val");
Customization of formats::*::Value::As<T>()
In order for formats::*::Value
to be able to represent data as a C++ type, you should write a special function Parse
for that C++ type. Parse
should be located in the namespace of the type or may be located in the formats::common
namespace if the type comes from third-party library that you have no control of:
namespace my_namespace {
struct MyKeyValue {
std::string field1;
int field2;
};
return MyKeyValue{
json["field1"].As<std::string>(""),
json["field2"].As<int>(1),
};
}
TEST(FormatsJson, ExampleUsageMyStruct) {
"my_value": {
"field1": "one",
"field2": 1
}
})");
auto data = json["my_value"].As<MyKeyValue>();
EXPECT_EQ(data.field1, "one");
EXPECT_EQ(data.field2, 1);
}
}
You can write a single parser for all formats, just make it a template:
USERVER_NAMESPACE_BEGIN
namespace my_namespace {
struct MyKeyValue {
std::string field1;
int field2;
};
template <class Value>
return MyKeyValue{
data["field1"].template As<std::string>(),
data["field2"].template As<int>(-1),
};
}
TEST(CommonFormats, Parse) {
"my_value": {
"field1": "one",
"field2": 1
}
})");
auto data_json = json["my_value"].As<MyKeyValue>();
EXPECT_EQ(data_json.field1, "one");
EXPECT_EQ(data_json.field2, 1);
my_value:
field1: "one"
field2: 1
)");
auto data_yaml = yaml["my_value"].As<MyKeyValue>();
EXPECT_EQ(data_yaml.field1, "one");
EXPECT_EQ(data_yaml.field2, 1);
}
}
Inline helpers formats::*::Make*
To build objects of trivial types some of the formats provide inline helpers, like formats::json::MakeArray(), formats::json::MakeObject():
"key1", 1,
"key2", "val",
"key5", 10.5,
"key6", false
);
Or formats::bson::MakeDoc(), formats::bson::MakeArray():
"key_a",
"key_d",
"key_n",
nullptr
);
auto umap = doc["key_d"].As<std::unordered_map<std::string, int>>();
EXPECT_EQ(1, umap["one"]);
EXPECT_EQ(2, umap["two"]);
Those inline helper functions usually work slightly faster than formats::*::ValueBuilder
. However, if you need a std::string
with JSON the fastest way would be to use the Streaming Serialization. Inline helpers could not be customized for used provided types, unlike other format building types. Inline helpers could produce broken value on bad input because they skip some of the checks, for example a key uniqueness check.
formats::*::ValueBuilder
Classes formats::json::ValueBuilder
, formats::bson::ValueBuilder
and formats::yaml::ValueBuilder
are designed for building objects of a given format.
Usage Example:
builder["key1"] = 1;
builder["key2"]["key3"] = "val";
ASSERT_EQ(json["key1"].As<int>(), 1);
ASSERT_EQ(json["key2"]["key3"].As<std::string>(), "val");
Customization of formats::*::ValueBuilder
In order for formats::*::ValueBuilder
to be able to represent a C++ type in the specified format, you should write a special function Serialize
for that C++ type. Serialize
should be located in the namespace of the type or may be located in the formats::common
namespace if the type comes from third-party library that you have no control of:
namespace my_namespace {
struct MyKeyValue {
std::string field1;
int field2;
};
builder["field1"] = data.field1;
builder["field2"] = data.field2;
}
TEST(JsonValueBuilder, ExampleCustomization) {
MyKeyValue object = {"val", 1};
builder["example"] = object;
ASSERT_EQ(json["example"]["field1"].As<std::string>(), "val");
ASSERT_EQ(json["example"]["field2"].As<int>(), 1);
}
TEST(JsonValueBuilder, StringViewRemove) {
const std::string str = "ab";
builder["a"] = 1;
builder[str] = 2;
builder.
Remove(std::string_view(str.data(), 1));
EXPECT_EQ(2, value[str].As<int>());
}
TEST(JsonValueBuilder, StringViewHasMember) {
const std::string str = "ab";
main_builder[str] = 2;
EXPECT_EQ(
false, main_builder.
HasMember(
"a"));
EXPECT_EQ(
false, main_builder.
HasMember(std::string_view(str.data(), 1)));
EXPECT_EQ(
true, main_builder.
HasMember(std::string_view(str.data(), 2)));
}
TEST(JsonValueBuilder, StringViewEmplaceNocheck) {
const std::string str = "ab";
main_builder.
EmplaceNocheck(std::string_view(std::string(1024,
'a') +
'b'), 1025);
EXPECT_EQ(1, value["a"].As<int>());
EXPECT_EQ(2, value["ab"].As<int>());
EXPECT_EQ(1025, value[std::string(1024, 'a') + 'b'].As<int>());
}
}
You can write a single serializer for all formats, for make it a template:
USERVER_NAMESPACE_BEGIN
namespace my_namespace {
struct MyKeyValue {
std::string field1;
int field2;
};
template <class Value>
typename Value::Builder builder;
builder["field1"] = data.field1;
builder["field2"] = data.field2;
return builder.ExtractValue();
}
TEST(CommonFormats, Serialize) {
MyKeyValue obj = {"val", 1};
builderJson["example"] = obj;
EXPECT_EQ(json["example"]["field1"].As<std::string>(), "val");
EXPECT_EQ(json["example"]["field2"].As<int>(), 1);
builderYaml["example"] = obj;
EXPECT_EQ(yaml["example"]["field1"].As<std::string>(), "val");
EXPECT_EQ(yaml["example"]["field2"].As<int>(), 1);
}
}
Streaming Serialization
For runtime-critical code, it is possible to use streaming serializers. They allow you to serialize several times faster than formats::json::ValueBuilder
, but should be used carefully because may produce broken format.
At the moment, stream serialization is implemented only for JSON via the formats::json::StringBuilder
.
In order for stream serialization to work with your data type, you need to define the WriteToStream
function in the namespace of your type:
namespace my_namespace {
struct MyKeyValue {
std::string field1;
int field2;
};
}
TEST(JsonStringBuilder, ExampleUsage) {
StringBuilder sb;
MyKeyValue data = {"one", 1};
ASSERT_EQ(sb.GetString(), "{\"field1\":\"one\",\"field2\":1}");
}
}
Note that you may get invalid JSON, since:
Test your serializers!