diff --git a/serde_json/contrib/message_to_json.cc b/serde_json/contrib/message_to_json.cc new file mode 100644 index 0000000..b7fab0f --- /dev/null +++ b/serde_json/contrib/message_to_json.cc @@ -0,0 +1,30 @@ +#include "contrib/message_to_json.h" + +#include + +#include "serde_json_bridge.h" +#include "absl/status/status.h" +#include "absl/status/statusor.h" +#include "third_party/protobuf/json/json.h" +#include "third_party/protobuf/message.h" + +namespace security::json::serde_json_bridge::contrib { + +absl::StatusOr MessageToJson( + const proto2::Message& message, const proto2::json::PrintOptions& options) { + std::string json_string; + absl::Status status = + proto2::json::MessageToJsonString(message, &json_string, options); + if (!status.ok()) { + return status; + } + return SerdeJson::Parse(json_string); +} + +absl::Status JsonToMessage(const SerdeJson& json, proto2::Message* message, + const proto2::json::ParseOptions& options) { + std::string json_string = json.ToString(/*sort_keys=*/false); + return proto2::json::JsonStringToMessage(json_string, message, options); +} + +} // namespace security::json::serde_json_bridge::contrib diff --git a/serde_json/contrib/message_to_json.h b/serde_json/contrib/message_to_json.h new file mode 100644 index 0000000..affb47e --- /dev/null +++ b/serde_json/contrib/message_to_json.h @@ -0,0 +1,39 @@ +#ifndef SECURITY_JSON_SERDE_JSON_CONTRIB_MESSAGE_TO_JSON_H_ +#define SECURITY_JSON_SERDE_JSON_CONTRIB_MESSAGE_TO_JSON_H_ + +#include + +#include "serde_json_bridge.h" +#include "absl/status/status.h" +#include "absl/status/statusor.h" +#include "third_party/protobuf/json/json.h" +#include "third_party/protobuf/message.h" + +namespace security::json::serde_json_bridge::contrib { + +// Converts a proto message to a SerdeJson object. +absl::StatusOr MessageToJson( + const proto2::Message& message, + const proto2::json::PrintOptions& options = proto2::json::PrintOptions()); + +// Converts a SerdeJson object to a proto message. +absl::Status JsonToMessage( + const SerdeJson& json, proto2::Message* message, + const proto2::json::ParseOptions& options = proto2::json::ParseOptions()); + +// Converts a SerdeJson object to a proto message. This will construct a proto +// message to populate with the JSON data. +template + requires std::derived_from +absl::StatusOr JsonToMessage( + const SerdeJson& json, + const proto2::json::ParseOptions& options = proto2::json::ParseOptions()) { + T message; + absl::Status status = JsonToMessage(json, &message, options); + if (!status.ok()) return status; + return message; +} + +} // namespace security::json::serde_json_bridge::contrib + +#endif // SECURITY_JSON_SERDE_JSON_CONTRIB_MESSAGE_TO_JSON_H_ diff --git a/serde_json/contrib/message_to_json_test.cc b/serde_json/contrib/message_to_json_test.cc new file mode 100644 index 0000000..78a595b --- /dev/null +++ b/serde_json/contrib/message_to_json_test.cc @@ -0,0 +1,108 @@ +#include "contrib/message_to_json.h" + +#include "contrib/messages.proto.h" +#include "serde_json_bridge.h" +#include "testing/base/public/gmock.h" +#include "testing/base/public/gunit.h" +#include "third_party/protobuf/json/json.h" + +namespace security::json::serde_json_bridge::contrib { +namespace { + +TEST(MessageToJsonTest, SimpleMessageToJson) { + SimpleMessage message; + message.set_string_field("hello"); + message.set_int32_field(123); + message.set_bool_field(true); + message.set_enum_field(SIMPLE_ENUM_A); + + ASSERT_OK_AND_ASSIGN(SerdeJson json, MessageToJson(message)); + + EXPECT_EQ( + json.ToString(), + R"json({"boolField":true,"enumField":"SIMPLE_ENUM_A","int32Field":123,"stringField":"hello"})json"); +} + +TEST(MessageToJsonTest, SimpleMessageToJsonAlwaysPrintEnumsAsInts) { + SimpleMessage message; + message.set_enum_field(SIMPLE_ENUM_A); + + proto2::json::PrintOptions options; + options.always_print_enums_as_ints = true; + + ASSERT_OK_AND_ASSIGN(SerdeJson json, MessageToJson(message, options)); + + EXPECT_EQ(json.ToString(), R"json({"enumField":1})json"); +} + +TEST(MessageToJsonTest, SimpleMessageRoundTrip) { + SimpleMessage message; + message.set_string_field("hello"); + message.set_int32_field(123); + message.set_bool_field(true); + message.set_enum_field(SIMPLE_ENUM_B); + + proto2::json::PrintOptions options; + + options.preserve_proto_field_names = true; + + ASSERT_OK_AND_ASSIGN(SerdeJson json, MessageToJson(message, options)); + ASSERT_OK_AND_ASSIGN(SimpleMessage parsed, + JsonToMessage(json)); + EXPECT_THAT(message, testing::EqualsProto(parsed)); +} + +TEST(MessageToJsonTest, SimpleMessageRoundTrip_WithOptions) { + proto2::json::PrintOptions print_options; + print_options.preserve_proto_field_names = true; + + SimpleMessage message; + message.set_string_field("hello"); + message.set_int32_field(123); + message.set_bool_field(true); + message.set_enum_field(SIMPLE_ENUM_C); + + ASSERT_OK_AND_ASSIGN(SerdeJson json, MessageToJson(message, print_options)); + + ASSERT_OK_AND_ASSIGN(SimpleMessage parsed, + JsonToMessage(json)); + EXPECT_THAT(message, testing::EqualsProto(parsed)); +} + +TEST(MessageToJsonTest, ComplexMessageRoundTrip) { + ComplexMessage message; + message.mutable_simple()->set_string_field("nested_hello"); + message.add_repeated_string("a"); + message.add_repeated_string("b"); + message.mutable_nested()->set_value("val"); + message.add_repeated_nested()->set_value("val1"); + message.add_repeated_nested()->set_value("val2"); + + ASSERT_OK_AND_ASSIGN(SerdeJson json, MessageToJson(message)); + + ASSERT_OK_AND_ASSIGN(ComplexMessage parsed, + JsonToMessage(json)); + ASSERT_THAT(message, testing::EqualsProto(parsed)); +} + +TEST(MessageToJsonTest, ComplexMessageRoundTrip_WithOptions) { + proto2::json::PrintOptions print_options; + print_options.preserve_proto_field_names = true; + + ComplexMessage message; + message.mutable_simple()->set_string_field("nested_hello"); + message.add_repeated_string("a"); + message.add_repeated_string("b"); + message.mutable_nested()->set_value("val"); + message.add_repeated_nested()->set_value("val1"); + message.add_repeated_nested()->set_value("val2"); + + ASSERT_OK_AND_ASSIGN(SerdeJson json, MessageToJson(message, print_options)); + + ASSERT_OK_AND_ASSIGN(ComplexMessage parsed, + JsonToMessage(json)); + ASSERT_THAT(message, testing::EqualsProto(parsed)); +} + +} // namespace +} // namespace security::json::serde_json_bridge::contrib diff --git a/serde_json/contrib/messages.proto b/serde_json/contrib/messages.proto new file mode 100644 index 0000000..093f8a8 --- /dev/null +++ b/serde_json/contrib/messages.proto @@ -0,0 +1,28 @@ +syntax = "proto3"; + +package security.json.serde_json_bridge.contrib; + +enum SimpleEnum { + SIMPLE_ENUM_UNSPECIFIED = 0; + SIMPLE_ENUM_A = 1; + SIMPLE_ENUM_B = 2; + SIMPLE_ENUM_C = 3; +} + +message SimpleMessage { + string string_field = 1; + int32 int32_field = 2; + bool bool_field = 3; + SimpleEnum enum_field = 4; +} + +message NestedMessage { + string value = 1; +} + +message ComplexMessage { + SimpleMessage simple = 1; + repeated string repeated_string = 2; + NestedMessage nested = 3; + repeated NestedMessage repeated_nested = 4; +}