Skip to content

Commit e58cf61

Browse files
committed
feat protobuf: add raw_any option to protobuf JSON conversion
* Added @ref protobuf::json::ParseOptions::nonportable_raw_any and @ref protobuf::json::PrintOptions::nonportable_raw_any that allow to pack arbitrary unknown `protobuf::Any` into JSON. commit_hash:dd9bccedf207fb59c1a07fa2ec7c28cf6fec32bf
1 parent 8fe445d commit e58cf61

6 files changed

Lines changed: 159 additions & 8 deletions

File tree

libraries/protobuf/include/userver/protobuf/json/convert_options.hpp

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,15 @@ struct ParseOptions {
1313
/// If not set, conversion of a JSON object containing unknown field (i.e. field that is not mapped to any protobuf
1414
/// message field) or unknown enum value name will fail.
1515
bool ignore_unknown_fields = false;
16+
17+
/// @brief Deserialize `google.protobuf.Any` from a raw `{"type_url": "...", "value": "<base64>"}`
18+
/// or `{"typeUrl": "...", "value": "<base64>"}` JSON object
19+
/// without looking up the payload descriptor in the descriptor pool.
20+
/// When set, `value` must be a base64-encoded protobuf wire-format string.
21+
///
22+
/// @warning This is a userver-only extension that is not supported in other frameworks.
23+
/// Only userver can correctly parse this format.
24+
bool nonportable_raw_any = false;
1625
};
1726

1827
/// @brief Options which affect how protobuf message is converted to a JSON `ValueBuilder`/`Value`.
@@ -35,6 +44,15 @@ struct PrintOptions {
3544
/// encoded in `lowerCamelCase` no matter whether `preserve_proto_field_names` is on or off. Moreover,
3645
/// `json_name` option is also not taken into account for `FieldMask` paths.
3746
bool preserve_proto_field_names = false;
47+
48+
/// @brief Serialize `google.protobuf.Any` as a raw `{"typeUrl": "...", "value": "<base64>"}` JSON object
49+
/// without looking up the payload descriptor in the descriptor pool.
50+
/// When set, `value` is base64-encoded protobuf wire bytes of the payload.
51+
/// If `preserve_proto_field_names` is true, `type_url` is used instead of `typeUrl`.
52+
///
53+
/// @warning This is a userver-only extension that is not supported in other frameworks.
54+
/// Only userver can correctly parse this format.
55+
bool nonportable_raw_any = false;
3856
};
3957

4058
} // namespace protobuf::json

libraries/protobuf/src/protobuf/json/impl/read.cpp

Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -956,6 +956,29 @@ void ReadAnyMessage(
956956
throw FieldError(ParseErrorCode::kInvalidType);
957957
}
958958

959+
if (options.nonportable_raw_any) {
960+
const bool has_proto_typeurl = json.HasMember("type_url");
961+
const bool has_nonproto_typeurl = json.HasMember("typeUrl");
962+
if (has_proto_typeurl == has_nonproto_typeurl) {
963+
throw FieldError(
964+
ParseErrorCode::kInvalidValue,
965+
"Exactly one of 'type_url' and 'typeUrl' msut be specified"
966+
);
967+
}
968+
const std::string_view type_url_field = has_proto_typeurl ? "type_url" : "typeUrl";
969+
970+
if (!json[type_url_field].IsString()) {
971+
throw FieldError(ParseErrorCode::kInvalidValue, "'type_url' field is not a string");
972+
}
973+
if (!json["value"].IsString()) {
974+
throw FieldError(ParseErrorCode::kInvalidValue, "'value' field is not a string");
975+
}
976+
977+
reflection.SetString(&message, &type_url_desc, json[type_url_field].As<std::string>());
978+
reflection.SetString(&message, &value_desc, crypto::base64::Base64Decode(json["value"].As<std::string>()));
979+
return;
980+
}
981+
959982
if (json.IsEmpty()) {
960983
return;
961984
}

libraries/protobuf/src/protobuf/json/impl/write.cpp

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7,8 +7,8 @@
77
#include <fmt/format.h>
88
#include <google/protobuf/descriptor.h>
99
#include <google/protobuf/dynamic_message.h>
10+
#include <google/protobuf/message.h>
1011
#include <google/protobuf/reflection.h>
11-
#include "google/protobuf/message.h"
1212

1313
#include <protobuf/json/impl/convert_utils.hpp>
1414
#include <protobuf/json/impl/field_error.hpp>
@@ -639,6 +639,12 @@ formats::json::ValueBuilder WriteAnyMessage(const ::google::protobuf::Message& m
639639

640640
formats::json::ValueBuilder object{formats::common::Type::kObject};
641641

642+
if (options.nonportable_raw_any) {
643+
object[options.preserve_proto_field_names ? "type_url" : "typeUrl"] = std::string_view{type_url};
644+
object["value"] = crypto::base64::Base64Encode(value);
645+
return object;
646+
}
647+
642648
if (type_url.empty() && value.empty()) {
643649
return object;
644650
}

libraries/protobuf/tests/json/any_from_json_test.cpp

Lines changed: 54 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -170,6 +170,39 @@ INSTANTIATE_TEST_SUITE_P(
170170
}
171171
})",
172172
AnyMessageData{ValueMessageData{std::map<std::string, std::string>{{"aaa", "hello"}, {"bbb", "world"}}}}
173+
},
174+
// nonportable_raw_any: parse Any from {"type_url": "...", "value": "<base64>"} without descriptor lookup
175+
AnyFromJsonSuccessTestParam{
176+
R"({"field1":{"type_url":"type.googleapis.com/proto_json.messages.Int32Message","value":"CAoQFBge"}})",
177+
AnyMessageData{
178+
RawAnyData{"type.googleapis.com/proto_json.messages.Int32Message", "\x08\x0a\x10\x14\x18\x1e"}
179+
},
180+
{.nonportable_raw_any = true},
181+
// Native implementation does not support nonportable_raw_any option.
182+
true
183+
},
184+
AnyFromJsonSuccessTestParam{
185+
R"({"field1":{"typeUrl":"type.googleapis.com/proto_json.messages.Int32Message","value":"CAoQFBge"}})",
186+
AnyMessageData{
187+
RawAnyData{"type.googleapis.com/proto_json.messages.Int32Message", "\x08\x0a\x10\x14\x18\x1e"}
188+
},
189+
{.nonportable_raw_any = true},
190+
// Native implementation does not support nonportable_raw_any option.
191+
true
192+
},
193+
AnyFromJsonSuccessTestParam{
194+
R"({"field1":{"type_url":"type.googleapis.com/proto_json.messages.NonExistent","value":"CAE="}})",
195+
AnyMessageData{RawAnyData{"type.googleapis.com/proto_json.messages.NonExistent", "\x08\x01"}},
196+
{.nonportable_raw_any = true},
197+
// Native implementation does not support nonportable_raw_any option.
198+
true
199+
},
200+
AnyFromJsonSuccessTestParam{
201+
R"({"field1":{"type_url":"","value":""}})",
202+
AnyMessageData{RawAnyData{"", ""}},
203+
{.nonportable_raw_any = true},
204+
// Native implementation does not support nonportable_raw_any option.
205+
true
173206
}
174207
)
175208
);
@@ -249,6 +282,27 @@ INSTANTIATE_TEST_SUITE_P(
249282
ParseErrorCode::kUnknownField,
250283
"field1.unknown_field",
251284
{.ignore_unknown_fields = false}
285+
},
286+
// nonportable_raw_any: missing 'value' field
287+
AnyFromJsonFailureTestParam{
288+
R"({"field1":{"type_url":"type.googleapis.com/some.Type"}})",
289+
ParseErrorCode::kInvalidValue,
290+
"field1",
291+
{.nonportable_raw_any = true}
292+
},
293+
// nonportable_raw_any: missing 'type_url' field
294+
AnyFromJsonFailureTestParam{
295+
R"({"field1":{"value":""}})",
296+
ParseErrorCode::kInvalidValue,
297+
"field1",
298+
{.nonportable_raw_any = true}
299+
},
300+
// nonportable_raw_any: non-string 'type_url'
301+
AnyFromJsonFailureTestParam{
302+
R"({"field1":{"type_url":123,"value":""}})",
303+
ParseErrorCode::kInvalidValue,
304+
"field1",
305+
{.nonportable_raw_any = true}
252306
}
253307
)
254308
);

libraries/protobuf/tests/json/any_to_json_test.cpp

Lines changed: 43 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -154,6 +154,49 @@ INSTANTIATE_TEST_SUITE_P(
154154
}
155155
}
156156
})"
157+
},
158+
// nonportable_raw_any: serialize Any as {"type_url": "...", "value": "<base64>"} without descriptor lookup
159+
AnyToJsonSuccessTestParam{
160+
AnyMessageData{Int32MessageData{1, 2, 3}},
161+
R"({"field1":{"type_url":"type.googleapis.com/proto_json.messages.Int32Message","value":"CAEQBB0DAAAA"}})",
162+
{.preserve_proto_field_names = true, .nonportable_raw_any = true},
163+
// Native implementation does not support nonportable_raw_any option.
164+
true
165+
},
166+
AnyToJsonSuccessTestParam{
167+
AnyMessageData{Int32MessageData{1, 2, 3}},
168+
R"({"field1":{"typeUrl":"type.googleapis.com/proto_json.messages.Int32Message","value":"CAEQBB0DAAAA"}})",
169+
{.nonportable_raw_any = true},
170+
// Native implementation does not support nonportable_raw_any option.
171+
true
172+
},
173+
AnyToJsonSuccessTestParam{
174+
AnyMessageData{RawAnyData{"type.googleapis.com/proto_json.messages.NonExistent", "\x08\x01"}},
175+
R"({"field1":{"type_url":"type.googleapis.com/proto_json.messages.NonExistent","value":"CAE="}})",
176+
{.preserve_proto_field_names = true, .nonportable_raw_any = true},
177+
// Native implementation does not support nonportable_raw_any option.
178+
true
179+
},
180+
AnyToJsonSuccessTestParam{
181+
AnyMessageData{RawAnyData{"type.googleapis.com/proto_json.messages.NonExistent", "\x08\x01"}},
182+
R"({"field1":{"typeUrl":"type.googleapis.com/proto_json.messages.NonExistent","value":"CAE="}})",
183+
{.nonportable_raw_any = true},
184+
// Native implementation does not support nonportable_raw_any option.
185+
true
186+
},
187+
AnyToJsonSuccessTestParam{
188+
AnyMessageData{RawAnyData{"", ""}},
189+
R"({"field1":{"type_url":"","value":""}})",
190+
{.preserve_proto_field_names = true, .nonportable_raw_any = true},
191+
// Native implementation does not support nonportable_raw_any option.
192+
true
193+
},
194+
AnyToJsonSuccessTestParam{
195+
AnyMessageData{RawAnyData{"", ""}},
196+
R"({"field1":{"typeUrl":"","value":""}})",
197+
{.nonportable_raw_any = true},
198+
// Native implementation does not support nonportable_raw_any option.
199+
true
157200
}
158201
)
159202
);

libraries/protobuf/tests/json/utils.hpp

Lines changed: 14 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -54,7 +54,7 @@ inline const /*deliberately non-constexpr*/ bool
5454

5555
template <>
5656
struct fmt::formatter<USERVER_NAMESPACE::protobuf::json::PrintOptions> {
57-
constexpr auto parse(fmt::format_parse_context& ctx) {
57+
auto parse(fmt::format_parse_context& ctx) {
5858
auto it = ctx.begin();
5959
if (it != ctx.end() && *it != '}') {
6060
throw fmt::format_error("invalid format");
@@ -67,17 +67,19 @@ struct fmt::formatter<USERVER_NAMESPACE::protobuf::json::PrintOptions> {
6767
-> decltype(ctx.out()) {
6868
return fmt::format_to(
6969
ctx.out(),
70-
"{{always_print_fields_with_no_presence={}, always_print_enums_as_ints={}, preserve_proto_field_names={}}}",
70+
"{{always_print_fields_with_no_presence={}, always_print_enums_as_ints={}, preserve_proto_field_names={}, "
71+
"nonportable_raw_any={}}}",
7172
options.always_print_fields_with_no_presence,
7273
options.always_print_enums_as_ints,
73-
options.preserve_proto_field_names
74+
options.preserve_proto_field_names,
75+
options.nonportable_raw_any
7476
);
7577
}
7678
};
7779

7880
template <>
7981
struct fmt::formatter<USERVER_NAMESPACE::protobuf::json::ParseOptions> {
80-
constexpr auto parse(fmt::format_parse_context& ctx) {
82+
auto parse(fmt::format_parse_context& ctx) {
8183
auto it = ctx.begin();
8284
if (it != ctx.end() && *it != '}') {
8385
throw fmt::format_error("invalid format");
@@ -88,13 +90,18 @@ struct fmt::formatter<USERVER_NAMESPACE::protobuf::json::ParseOptions> {
8890
template <typename FormatContext>
8991
auto format(const USERVER_NAMESPACE::protobuf::json::ParseOptions& options, FormatContext& ctx) const
9092
-> decltype(ctx.out()) {
91-
return fmt::format_to(ctx.out(), "{{ignore_unknown_fields={}}}", options.ignore_unknown_fields);
93+
return fmt::format_to(
94+
ctx.out(),
95+
"{{ignore_unknown_fields={}, nonportable_raw_any={}}}",
96+
options.ignore_unknown_fields,
97+
options.nonportable_raw_any
98+
);
9299
}
93100
};
94101

95102
template <>
96103
struct fmt::formatter<USERVER_NAMESPACE::protobuf::json::PrintErrorCode> {
97-
constexpr auto parse(fmt::format_parse_context& ctx) {
104+
auto parse(fmt::format_parse_context& ctx) {
98105
auto it = ctx.begin();
99106
if (it != ctx.end() && *it != '}') {
100107
throw fmt::format_error("invalid format");
@@ -116,7 +123,7 @@ struct fmt::formatter<USERVER_NAMESPACE::protobuf::json::PrintErrorCode> {
116123

117124
template <>
118125
struct fmt::formatter<USERVER_NAMESPACE::protobuf::json::ParseErrorCode> {
119-
constexpr auto parse(fmt::format_parse_context& ctx) {
126+
auto parse(fmt::format_parse_context& ctx) {
120127
auto it = ctx.begin();
121128
if (it != ctx.end() && *it != '}') {
122129
throw fmt::format_error("invalid format");

0 commit comments

Comments
 (0)