summaryrefslogtreecommitdiff
path: root/libjsonpb
diff options
context:
space:
mode:
authorYifan Hong <elsk@google.com>2019-02-13 14:29:33 -0800
committerYifan Hong <elsk@google.com>2019-02-14 16:17:33 -0800
commit4d68f66ac627eea44439459e5cd1723e7b15000a (patch)
treed5815adc253f42265ca6c60e6ee01002771b730f /libjsonpb
parentcb7a7ab24d394d62935fb8397f3c0a964e2bf1a7 (diff)
downloadextras-4d68f66ac627eea44439459e5cd1723e7b15000a.tar.gz
Add libjsonpbverify.
- libjsonpbverify is a helper library that a gtest can link against when checking JSON files against their protobuf schema. - Add unittests to ensure these validation functions are correct. (libjsonpbverify_test) - Add tests to mitigate discrepancies of behavior between libjsoncpp and libprocessgroup. (libjsonpbverify_test) - Add tests to impose additional restrictions on proto3 files when using them as JSON schema. Test: libjsonpbverify_test Bug: 123664216 Change-Id: I1e6257c9a25fd8d10fad103f3b74f7e1c6fad8a6
Diffstat (limited to 'libjsonpb')
-rw-r--r--libjsonpb/README.md61
-rw-r--r--libjsonpb/TEST_MAPPING8
-rw-r--r--libjsonpb/verify/Android.bp70
-rw-r--r--libjsonpb/verify/include/jsonpb/json_schema_test.h112
-rw-r--r--libjsonpb/verify/include/jsonpb/verify.h93
-rw-r--r--libjsonpb/verify/test.cpp302
-rw-r--r--libjsonpb/verify/test.proto65
-rw-r--r--libjsonpb/verify/verify.cpp221
8 files changed, 932 insertions, 0 deletions
diff --git a/libjsonpb/README.md b/libjsonpb/README.md
index d8bf6e22..5562c8f7 100644
--- a/libjsonpb/README.md
+++ b/libjsonpb/README.md
@@ -44,3 +44,64 @@ logic of `libjsoncpp` and `libprotobuf` when parsing JSON files.
Once `libprotobuf` in the source tree is updated to a higher version and
`libjsonpbparse` is updated to ignore unknown fields in JSON files, all parsing
code must be converted to use `libjsonpbparse` for consistency.
+
+# `libjsonpbverify`
+
+This library provides functions and tests to examine a JSON file and validate
+it against a Protobuf message definition.
+
+In addition to a sanity check that `libprotobuf` can convert the JSON file to a
+Protobuf message (using `libjsonpbparse`), it also checks the following:
+
+- Whether there are fields unknown to the schema. All fields in the JSON file
+ must be well defined in the schema.
+- Whether the Protobuf file defines JSON keys clearly. The JSON keys must be
+ the `json_name` option of a Protobuf field, or name of a Protobuf field if
+ `json_name` is not defined. `lowerCamelCase` supported by `libprotobuf` is
+ explicitly disallowed (unless explicitly used in `json_name`). For example,
+ in the following Protobuf file, only keys `foo_bar` and `barBaz` are allowed
+ in the JSON file:
+ ```
+ message Foo {
+ string foo_bar = 1;
+ string bar_baz = 2 [json_name = "barBaz"];
+ }
+ ```
+- Whether `json == convert_to_json(convert_to_pb(json))`, using `libprotobuf`.
+ This imposes additional restrictions including:
+ - Enum values must be present as names (not integer values) in the JSON file.
+ - 64-bit integers and special floating point values (infinity, NaN) must
+ always be strings.
+
+## Defining a JSON schema using Protobuf
+
+Check [JSON Mapping](https://developers.google.com/protocol-buffers/docs/proto3#json)
+before defining a Protobuf object as a JSON schema. In general:
+
+- **Use proto3**. `libjsonverify` does not support proto2.
+- JSON booleans should be `bool`.
+- JSON numbers should be `(s|fixed|u|)int32`, `float`, or `double` in the schema
+- JSON strings are generally `string`s, but if you want to impose more
+ restrictions on the string, you can also use `Timestamp`, `bytes`,
+ **`float`** or **`double`** (if NaN and infinity are valid values),
+ enumerations, etc.
+ - If a custom enumeration is used, parser code should **NOT** error when the
+ enumeration value name is unknown, as enumeration definitions may be
+ extended in the future.
+- JSON arrays should be repeated fields.
+- JSON objects should be a well-defined `message`, unless you have a good reason
+ to use `map<string, T>`.
+- Don't use `Any`; it defeats the purpose of having the schema.
+
+## Validating a JSON file against a Protobuf definition
+
+Example:
+```c++
+#include <jsonpb/verify.h>
+using namespace ::android::jsonpb;
+std::unique_ptr<JsonSchemaTestConfig> CreateCgroupsParam() {
+
+}
+INSTANTIATE_TEST_SUITE_P(LibProcessgroupProto, JsonSchemaTest,
+ ::testing::Values(MakeTestParam<Cgroups>("cgroups.json")));
+```
diff --git a/libjsonpb/TEST_MAPPING b/libjsonpb/TEST_MAPPING
new file mode 100644
index 00000000..69e5a25d
--- /dev/null
+++ b/libjsonpb/TEST_MAPPING
@@ -0,0 +1,8 @@
+{
+ "presubmit": [
+ {
+ "name": "libjsonpbverify_test",
+ "host": true
+ }
+ ]
+}
diff --git a/libjsonpb/verify/Android.bp b/libjsonpb/verify/Android.bp
new file mode 100644
index 00000000..b32b9b4f
--- /dev/null
+++ b/libjsonpb/verify/Android.bp
@@ -0,0 +1,70 @@
+// Copyright (C) 2019 The Android Open Source Project
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+// This static library defines parameterized tests that enforce additional restrictions when
+// using Protobuf as schema for JSON files. The reason is that the JSON parser that
+// libprotobuf-cpp-full provides is relatively relaxed.
+cc_library_static {
+ name: "libjsonpbverify",
+ host_supported: true,
+ srcs: [
+ "verify.cpp",
+ ],
+ shared_libs: [
+ "libbase",
+ "libprotobuf-cpp-full",
+ "libjsoncpp",
+ ],
+ static_libs: [
+ "libjsonpbparse",
+ ],
+ export_static_lib_headers: [
+ "libjsonpbparse",
+ ],
+ export_include_dirs: [
+ "include",
+ ],
+ cflags: [
+ "-Wall",
+ "-Werror",
+ "-Wno-unused-parameter",
+ ],
+}
+
+cc_test_host {
+ name: "libjsonpbverify_test",
+ srcs: [
+ "test.cpp",
+ "test.proto",
+ ],
+ static_libs: [
+ "libbase",
+ "liblog",
+ "libgmock",
+ "libjsoncpp",
+ "libjsonpbparse",
+ "libjsonpbverify",
+ ],
+ shared_libs: [
+ "libprotobuf-cpp-full",
+ ],
+ cflags: [
+ "-Wall",
+ "-Werror",
+ "-Wno-unused-parameter",
+ ],
+ test_suites: [
+ "general-tests",
+ ],
+}
diff --git a/libjsonpb/verify/include/jsonpb/json_schema_test.h b/libjsonpb/verify/include/jsonpb/json_schema_test.h
new file mode 100644
index 00000000..9a62ea9a
--- /dev/null
+++ b/libjsonpb/verify/include/jsonpb/json_schema_test.h
@@ -0,0 +1,112 @@
+/*
+ * Copyright (C) 2019 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+
+#pragma once
+
+#include <memory>
+#include <string>
+
+#include <android-base/file.h>
+#include <android-base/strings.h>
+#include <gtest/gtest.h>
+#include <json/reader.h>
+#include <json/writer.h>
+#include <jsonpb/jsonpb.h>
+#include <jsonpb/verify.h>
+
+// JsonSchemaTest test that a given JSON file conforms to a given schema.
+// This includes:
+// - libprotobuf can parse the given JSON file using the given Prototype class
+// - Additional checks on field names of the JSON file, and types of values.
+
+namespace android {
+namespace jsonpb {
+
+class JsonSchemaTestConfig {
+ public:
+ virtual ~JsonSchemaTestConfig() = default;
+ virtual std::unique_ptr<google::protobuf::Message> CreateMessage() const = 0;
+ virtual std::string file_path() const = 0;
+ virtual std::string GetFileContent() const {
+ std::string content;
+ if (!android::base::ReadFileToString(file_path(), &content)) {
+ return "";
+ }
+ return content;
+ }
+};
+using JsonSchemaTestConfigFactory =
+ std::function<std::unique_ptr<JsonSchemaTestConfig>()>;
+
+template <typename T>
+class AbstractJsonSchemaTestConfig : public JsonSchemaTestConfig {
+ public:
+ AbstractJsonSchemaTestConfig(const std::string& path) : file_path_(path){};
+ std::unique_ptr<google::protobuf::Message> CreateMessage() const override {
+ return std::make_unique<T>();
+ }
+ std::string file_path() const override { return file_path_; }
+
+ private:
+ std::string file_path_;
+};
+
+template <typename T>
+JsonSchemaTestConfigFactory MakeTestParam(const std::string& path) {
+ return [path]() {
+ return std::make_unique<AbstractJsonSchemaTestConfig<T>>(path);
+ };
+}
+
+class JsonSchemaTest
+ : public ::testing::TestWithParam<JsonSchemaTestConfigFactory> {
+ public:
+ void SetUp() override {
+ auto&& config =
+ ::testing::TestWithParam<JsonSchemaTestConfigFactory>::GetParam()();
+ file_path_ = config->file_path();
+ json_ = config->GetFileContent();
+ ASSERT_FALSE(json_.empty()) << "Cannot read " << config->file_path();
+ object_ = config->CreateMessage();
+ auto res = internal::JsonStringToMessage(json_, object_.get());
+ ASSERT_TRUE(res.ok()) << "Invalid format of file " << config->file_path()
+ << ": " << res.error();
+ }
+ google::protobuf::Message* message() const {
+ return object_.get();
+ }
+ std::string file_path_;
+ std::string json_;
+ std::unique_ptr<google::protobuf::Message> object_;
+};
+
+// Test that the JSON file has no fields unknown by the schema. See
+// AllFieldsAreKnown() for more details.
+TEST_P(JsonSchemaTest, NoUnknownFields) {
+ std::string error;
+ EXPECT_TRUE(AllFieldsAreKnown(*object_, json_, &error))
+ << "File: " << file_path_ << ": " << error;
+}
+
+TEST_P(JsonSchemaTest, EqReformattedJson) {
+ std::string error;
+ EXPECT_TRUE(EqReformattedJson(json_, object_.get(), &error))
+ << "File: " << file_path_ << ": " << error;
+}
+
+} // namespace jsonpb
+} // namespace android
diff --git a/libjsonpb/verify/include/jsonpb/verify.h b/libjsonpb/verify/include/jsonpb/verify.h
new file mode 100644
index 00000000..c05b13d2
--- /dev/null
+++ b/libjsonpb/verify/include/jsonpb/verify.h
@@ -0,0 +1,93 @@
+/*
+ * Copyright (C) 2019 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+
+#pragma once
+
+#include <sstream>
+#include <string>
+#include <vector>
+
+#include <google/protobuf/message.h>
+#include <json/reader.h>
+#include <json/value.h>
+#include <jsonpb/jsonpb.h>
+
+namespace android {
+namespace jsonpb {
+
+// Ensure that the JSON file has no unknown fields that is not defined in proto.
+// Because we want forwards compatibility, the parser of JSON files must ignore
+// unknown fields. This is achievable with libprotobuf version > 3.0-beta.
+// - <= 3.0-beta: we have to check unknown fields manually, and parser cannot
+// use libprotobuf
+// to parse JSON files.
+// - < 3.5: libprotobuf discards all unknown fields. We can still check unknown
+// fields manually, but
+// an easier way to check is `json == FormatJson(json)` (schematically)
+// - >= 3.5: Unknown fields are preserved, so FormatJson() may contain these
+// unknown fields. We can
+// still check fields manually, or use reflection mechanism.
+//
+// For example, if a new field "foo" is added to cgroups.json but not to
+// cgroups.proto, libprocessgroup could technically read the value of "foo" by
+// using other libraries that parse JSON strings, effectively working around the
+// schema.
+//
+// This test also ensures that the parser does not use alternative key names.
+// For example, if the proto file states: message Foo { string foo_bar = 1;
+// string bar_baz = 2 [json_name = "BarBaz"]; } Then the parser accepts
+// "foo_bar" "fooBar", "bar_baz", "BarBaz" as valid key names. Here, we enforce
+// that the JSON file must use "foo_bar" and "BarBaz".
+//
+// Requiring this avoids surprises like:
+// message Foo { string FooBar = 1; }
+// { "fooBar" : "s" }
+// conforms with the schema, because libprotobuf accept "fooBar" as a valid key.
+// The correct schema should be:
+// message Foo { string foo_bar = 1 [json_name="fooBar"]; }
+//
+// Params:
+// path: path to navigate inside JSON tree. For example, {"foo", "bar"} for
+// the value "string" in
+// {"foo": {"bar" : "string"}}
+bool AllFieldsAreKnown(const google::protobuf::Message& message,
+ const std::string& json, std::string* error);
+
+// Format the given JSON string according to Prototype T. This will serialize
+// the JSON string to a Prototype message, then re-print the message as JSON. By
+// reformatting the JSON string, we effectively enforces that the JSON source
+// file uses conventions of Protobuf's JSON writer; e.g. 64-bit integers /
+// special floating point numbers (inf, NaN, etc.) in strings, enum values in
+// names, etc.
+//
+// Params:
+// scratch_space: The scratch space to use to store the Protobuf message. It
+// must be a pointer
+// to the schema that the JSON string conforms to.
+bool EqReformattedJson(const std::string& json,
+ google::protobuf::Message* scratch_space,
+ std::string* error);
+
+namespace internal {
+// See EqReformattedJson().
+ErrorOr<std::string> FormatJson(const std::string& json,
+ google::protobuf::Message* scratch_space);
+
+} // namespace internal
+
+} // namespace jsonpb
+} // namespace android
diff --git a/libjsonpb/verify/test.cpp b/libjsonpb/verify/test.cpp
new file mode 100644
index 00000000..2ffc923a
--- /dev/null
+++ b/libjsonpb/verify/test.cpp
@@ -0,0 +1,302 @@
+/*
+ * Copyright (C) 2019 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+
+#include <limits>
+
+#include <sstream>
+
+#include <gmock/gmock.h>
+#include <gtest/gtest.h>
+#include <json/writer.h>
+#include <jsonpb/jsonpb.h>
+#include <jsonpb/verify.h>
+
+#include "test.pb.h"
+
+using ::android::jsonpb::internal::FormatJson;
+using ::testing::ElementsAre;
+using ::testing::HasSubstr;
+
+namespace android {
+namespace jsonpb {
+
+// Unit tests for libjsonpbverify.
+
+class LibJsonpbVerifyTest : public ::testing::Test {};
+
+class JsonKeyTest : public LibJsonpbVerifyTest {
+ public:
+ template <typename T>
+ std::string GetFieldJsonName(const std::string& field_name) {
+ return T{}.GetDescriptor()->FindFieldByName(field_name)->json_name();
+ }
+
+ template <typename T>
+ void TestParseOkWithUnknownKey(const std::string& field_name,
+ const std::string& json_key) {
+ std::string json = "{\"" + json_key + "\": \"test\"}";
+ auto object = JsonStringToMessage<T>(json);
+ ASSERT_TRUE(object.ok()) << object.error();
+ EXPECT_EQ(
+ "test",
+ object->GetReflection()->GetString(
+ *object, object->GetDescriptor()->FindFieldByName(field_name)));
+ std::string error;
+ ASSERT_FALSE(AllFieldsAreKnown(*object, json, &error))
+ << "AllFieldsAreKnown should return false";
+ EXPECT_THAT(error, HasSubstr("unknown keys"));
+ EXPECT_THAT(error, HasSubstr(json_key));
+ }
+};
+
+TEST_F(JsonKeyTest, WithJsonNameOk) {
+ std::string json =
+ "{\n"
+ " \"FOOBAR\": \"foo_bar\",\n"
+ " \"BarBaz\": \"barBaz\",\n"
+ " \"baz_qux\": \"BazQux\",\n"
+ " \"quxQuux\": \"QUX_QUUX\"\n"
+ "\n}";
+ auto object = JsonStringToMessage<WithJsonName>(json);
+ ASSERT_TRUE(object.ok()) << object.error();
+
+ EXPECT_EQ("foo_bar", object->foo_bar());
+ EXPECT_EQ("barBaz", object->barbaz());
+ EXPECT_EQ("BazQux", object->bazqux());
+ EXPECT_EQ("QUX_QUUX", object->qux_quux());
+
+ std::string error;
+ EXPECT_TRUE(AllFieldsAreKnown(*object, json, &error)) << error;
+}
+
+// If Prototype field name as keys while json_name is present, AllFieldsAreKnown
+// should return false.
+TEST_F(JsonKeyTest, WithJsonNameFooBar) {
+ TestParseOkWithUnknownKey<WithJsonName>("foo_bar", "foo_bar");
+}
+
+TEST_F(JsonKeyTest, WithJsonNameBarBaz) {
+ TestParseOkWithUnknownKey<WithJsonName>("barBaz", "barBaz");
+}
+
+TEST_F(JsonKeyTest, WithJsonNameBazQux) {
+ TestParseOkWithUnknownKey<WithJsonName>("BazQux", "BazQux");
+}
+
+TEST_F(JsonKeyTest, WithJsonNameQuxQuux) {
+ TestParseOkWithUnknownKey<WithJsonName>("QUX_QUUX", "QUX_QUUX");
+}
+
+// JSON field name matches Proto field name
+TEST_F(JsonKeyTest, NoJsonNameOk) {
+ std::string json =
+ "{\n"
+ " \"foo_bar\": \"foo_bar\",\n"
+ " \"barBaz\": \"barBaz\",\n"
+ " \"BazQux\": \"BazQux\",\n"
+ " \"QUX_QUUX\": \"QUX_QUUX\"\n"
+ "\n}";
+ auto object = JsonStringToMessage<NoJsonName>(json);
+ ASSERT_TRUE(object.ok()) << object.error();
+
+ EXPECT_EQ("foo_bar", object->foo_bar());
+ EXPECT_EQ("barBaz", object->barbaz());
+ EXPECT_EQ("BazQux", object->bazqux());
+ EXPECT_EQ("QUX_QUUX", object->qux_quux());
+
+ std::string error;
+ EXPECT_TRUE(AllFieldsAreKnown(*object, json, &error)) << error;
+}
+
+// JSON field name is lowerCamelCase of Proto field name;
+// AllFieldsAreKnown should return false. Although the lowerCamelCase name is a
+// valid key accepted by Protobuf's JSON parser, we explicitly disallow the
+// behavior.
+TEST_F(JsonKeyTest, NoJsonNameFooBar) {
+ EXPECT_EQ("fooBar", GetFieldJsonName<NoJsonName>("foo_bar"));
+ TestParseOkWithUnknownKey<NoJsonName>("foo_bar", "fooBar");
+}
+
+TEST_F(JsonKeyTest, NoJsonNameBarBaz) {
+ EXPECT_EQ("barBaz", GetFieldJsonName<NoJsonName>("barBaz"));
+ // No test for barBaz because its JSON name is the same as field_name
+}
+
+TEST_F(JsonKeyTest, NoJsonNameBazQux) {
+ EXPECT_EQ("bazQux", GetFieldJsonName<NoJsonName>("BazQux"));
+ TestParseOkWithUnknownKey<NoJsonName>("BazQux", "bazQux");
+}
+
+TEST_F(JsonKeyTest, NoJsonNameQuxQuux) {
+ EXPECT_EQ("qUXQUUX", GetFieldJsonName<NoJsonName>("QUX_QUUX"));
+ TestParseOkWithUnknownKey<NoJsonName>("QUX_QUUX", "qUXQUUX");
+}
+
+class EmbeddedJsonKeyTest : public LibJsonpbVerifyTest {
+ public:
+ ErrorOr<Parent> TestEmbeddedError(const std::string& json,
+ const std::string& unknown_key) {
+ auto object = JsonStringToMessage<Parent>(json);
+ if (!object.ok()) return object;
+ std::string error;
+ EXPECT_FALSE(AllFieldsAreKnown(*object, json, &error))
+ << "AllFieldsAreKnown should return false";
+ EXPECT_THAT(error, HasSubstr("unknown keys"));
+ EXPECT_THAT(error, HasSubstr(unknown_key));
+ return object;
+ }
+};
+
+TEST_F(EmbeddedJsonKeyTest, Ok) {
+ std::string json =
+ "{"
+ " \"with_json_name\": {\"FOOBAR\": \"foo_bar\"},\n"
+ " \"repeated_with_json_name\": [{\"BarBaz\": \"barBaz\"}],\n"
+ " \"no_json_name\": {\"BazQux\": \"BazQux\"},\n"
+ " \"repeated_no_json_name\": [{\"QUX_QUUX\": \"QUX_QUUX\"}]\n"
+ "}";
+ auto object = JsonStringToMessage<Parent>(json);
+ ASSERT_TRUE(object.ok()) << object.error();
+
+ EXPECT_EQ("foo_bar", object->with_json_name().foo_bar());
+ ASSERT_EQ(1u, object->repeated_with_json_name().size());
+ EXPECT_EQ("barBaz", object->repeated_with_json_name().begin()->barbaz());
+ EXPECT_EQ("BazQux", object->no_json_name().bazqux());
+ ASSERT_EQ(1u, object->repeated_no_json_name().size());
+ EXPECT_EQ("QUX_QUUX", object->repeated_no_json_name().begin()->qux_quux());
+
+ std::string error;
+ EXPECT_TRUE(AllFieldsAreKnown(*object, json, &error)) << error;
+}
+
+TEST_F(EmbeddedJsonKeyTest, FooBar) {
+ auto object = TestEmbeddedError(
+ "{\"with_json_name\": {\"foo_bar\": \"test\"}}", "foo_bar");
+ ASSERT_TRUE(object.ok()) << object.error();
+ EXPECT_EQ("test", object->with_json_name().foo_bar());
+}
+
+TEST_F(EmbeddedJsonKeyTest, BarBaz) {
+ auto object = TestEmbeddedError(
+ "{\"repeated_with_json_name\": [{\"barBaz\": \"test\"}]}", "barBaz");
+ ASSERT_TRUE(object.ok()) << object.error();
+ ASSERT_EQ(1u, object->repeated_with_json_name().size());
+ EXPECT_EQ("test", object->repeated_with_json_name().begin()->barbaz());
+}
+
+TEST_F(EmbeddedJsonKeyTest, BazQux) {
+ auto object =
+ TestEmbeddedError("{\"no_json_name\": {\"bazQux\": \"test\"}}", "bazQux");
+ ASSERT_TRUE(object.ok()) << object.error();
+ EXPECT_EQ("test", object->no_json_name().bazqux());
+}
+
+TEST_F(EmbeddedJsonKeyTest, QuxQuux) {
+ auto object = TestEmbeddedError(
+ "{\"repeated_no_json_name\": [{\"qUXQUUX\": \"test\"}]}", "qUXQUUX");
+ ASSERT_TRUE(object.ok()) << object.error();
+ ASSERT_EQ(1u, object->repeated_no_json_name().size());
+ EXPECT_EQ("test", object->repeated_no_json_name().begin()->qux_quux());
+}
+
+class ScalarTest : public LibJsonpbVerifyTest {
+ public:
+ ::testing::AssertionResult IsJsonEq(const std::string& l,
+ const std::string& r) {
+ Json::Reader reader;
+ Json::Value lvalue;
+ if (!reader.parse(l, lvalue))
+ return ::testing::AssertionFailure()
+ << reader.getFormattedErrorMessages();
+ Json::Value rvalue;
+ if (!reader.parse(r, rvalue))
+ return ::testing::AssertionFailure()
+ << reader.getFormattedErrorMessages();
+ Json::StyledWriter writer;
+ return lvalue == rvalue
+ ? (::testing::AssertionSuccess() << "Both are \n"
+ << writer.write(lvalue))
+ : (::testing::AssertionFailure()
+ << writer.write(lvalue) << "\n does not equal \n"
+ << writer.write(rvalue));
+ }
+
+ bool EqReformattedJson(const std::string& json, std::string* error) {
+ return android::jsonpb::EqReformattedJson(json, &scalar_, error);
+ }
+
+ Scalar scalar_;
+ std::string error_;
+};
+
+TEST_F(ScalarTest, Ok) {
+ std::string json =
+ "{\n"
+ " \"i32\": 1,\n"
+ " \"si32\": 1,\n"
+ " \"i64\": \"1\",\n"
+ " \"si64\": \"1\",\n"
+ " \"f\": 1.5,\n"
+ " \"d\": 1.5,\n"
+ " \"e\": \"FOO\"\n"
+ "}";
+ auto formatted = FormatJson(json, &scalar_);
+ ASSERT_TRUE(formatted.ok()) << formatted.error();
+ EXPECT_TRUE(IsJsonEq(json, *formatted));
+
+ EXPECT_TRUE(EqReformattedJson(json, &error_)) << error_;
+}
+
+using ScalarTestErrorParam = std::tuple<const char*, const char*>;
+class ScalarTestError
+ : public ScalarTest,
+ public ::testing::WithParamInterface<ScalarTestErrorParam> {};
+
+TEST_P(ScalarTestError, Test) {
+ std::string json;
+ std::string message;
+ std::tie(json, message) = GetParam();
+ auto formatted = FormatJson(json, &scalar_);
+ ASSERT_TRUE(formatted.ok()) << formatted.error();
+ EXPECT_FALSE(IsJsonEq(json, *formatted)) << message;
+ EXPECT_FALSE(EqReformattedJson(json, &error_))
+ << "EqReformattedJson should return false";
+}
+
+static const std::vector<ScalarTestErrorParam> gScalarTestErrorParams = {
+ {"{\"i32\": \"1\"}", "Should not allow int32 values to be quoted"},
+ {"{\"si32\": \"1\"}", "Should not allow sint32 values to be quoted"},
+ {"{\"i64\": 1}", "Should require int64 values to be quoted"},
+ {"{\"si64\": 1}", "Should require sint64 values to be quoted"},
+ {"{\"f\": \"1.5\"}", "Should not allow float values to be quoted"},
+ {"{\"d\": \"1.5\"}", "Should not allow double values to be quoted"},
+ {"{\"e\": 1}", "Should not allow integers for enums"},
+};
+
+INSTANTIATE_TEST_SUITE_P(, ScalarTestError,
+ ::testing::ValuesIn(gScalarTestErrorParams));
+
+int main(int argc, char** argv) {
+ using ::testing::AddGlobalTestEnvironment;
+ using ::testing::InitGoogleTest;
+
+ InitGoogleTest(&argc, argv);
+ return RUN_ALL_TESTS();
+}
+
+} // namespace jsonpb
+} // namespace android
diff --git a/libjsonpb/verify/test.proto b/libjsonpb/verify/test.proto
new file mode 100644
index 00000000..29ec8b14
--- /dev/null
+++ b/libjsonpb/verify/test.proto
@@ -0,0 +1,65 @@
+/*
+ * Copyright (C) 2019 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+syntax = "proto3";
+
+package android.jsonpb;
+
+// Note: this file explicitly uses names that does NOT follow the Protobuf Style
+// Guide for testing purposes. When writing a .proto file as a JSON schema, you
+// should:
+// - Follow the Protobuf Style Guide for field names / enum value names
+// - If the JSON file is going to have field names that does not conform to the
+// Protobuf Style Guide (a.k.a lower_snake_case), use json_name option to
+// indicate an alternative name.
+// - If the JSON file is going to have enum value names that does not conform to
+// the Protobuf Style Guide (a.k.a CAPITALIZED_SNAKE_CASE), use strings.
+
+message WithJsonName {
+ string foo_bar = 1 [json_name = "FOOBAR"];
+ string barBaz = 2 [json_name = "BarBaz"];
+ string BazQux = 3 [json_name = "baz_qux"];
+ string QUX_QUUX = 4 [json_name = "quxQuux"];
+}
+
+message NoJsonName {
+ string foo_bar = 1;
+ string barBaz = 2;
+ string BazQux = 3;
+ string QUX_QUUX = 4;
+}
+
+message Parent {
+ repeated WithJsonName repeated_with_json_name = 1;
+ WithJsonName with_json_name = 2;
+ repeated NoJsonName repeated_no_json_name = 3;
+ NoJsonName no_json_name = 4;
+}
+
+message Scalar {
+ int32 i32 = 1;
+ sint32 si32 = 2;
+ int64 i64 = 3;
+ sint64 si64 = 4;
+ float f = 5;
+ double d = 6;
+
+ enum Enum {
+ DEFAULT = 0;
+ FOO = 1;
+ }
+ Enum e = 7;
+}
diff --git a/libjsonpb/verify/verify.cpp b/libjsonpb/verify/verify.cpp
new file mode 100644
index 00000000..c411de81
--- /dev/null
+++ b/libjsonpb/verify/verify.cpp
@@ -0,0 +1,221 @@
+/*
+ * Copyright (C) 2019 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+
+#include <jsonpb/verify.h>
+
+#include <iostream>
+#include <memory>
+#include <sstream>
+#include <string>
+
+#include <android-base/strings.h>
+#include <google/protobuf/descriptor.h>
+#include <google/protobuf/descriptor.pb.h>
+#include <google/protobuf/message.h>
+#include <google/protobuf/reflection.h>
+#include <json/reader.h>
+#include <json/writer.h>
+#include <jsonpb/jsonpb.h>
+
+namespace android {
+namespace jsonpb {
+
+using google::protobuf::FieldDescriptor;
+using google::protobuf::FieldDescriptorProto;
+using google::protobuf::Message;
+
+// Return json_name of the field. If it is not set, return the name of the
+// field.
+const std::string& GetJsonName(const FieldDescriptor& field_descriptor) {
+ // The current version of libprotobuf does not define
+ // FieldDescriptor::has_json_name() yet. Use a workaround.
+ // TODO: use field_descriptor.has_json_name() when libprotobuf version is
+ // bumped.
+ FieldDescriptorProto proto;
+ field_descriptor.CopyTo(&proto);
+ return proto.has_json_name() ? field_descriptor.json_name()
+ : field_descriptor.name();
+}
+
+bool AllFieldsAreKnown(const Message& message, const Json::Value& json,
+ std::vector<std::string>* path,
+ std::stringstream* error) {
+ if (!json.isObject()) {
+ *error << base::Join(*path, ".") << ": Not a JSON object\n";
+ return false;
+ }
+ auto&& descriptor = message.GetDescriptor();
+
+ auto json_members = json.getMemberNames();
+ std::set<std::string> json_keys{json_members.begin(), json_members.end()};
+
+ std::set<std::string> known_keys;
+ for (int i = 0; i < descriptor->field_count(); ++i) {
+ known_keys.insert(GetJsonName(*descriptor->field(i)));
+ }
+
+ std::set<std::string> unknown_keys;
+ std::set_difference(json_keys.begin(), json_keys.end(), known_keys.begin(),
+ known_keys.end(),
+ std::inserter(unknown_keys, unknown_keys.begin()));
+
+ if (!unknown_keys.empty()) {
+ *error << base::Join(*path, ".") << ": contains unknown keys: ["
+ << base::Join(unknown_keys, ", ")
+ << "]. Keys must be a known field name of "
+ << descriptor->full_name() << "(or its json_name option if set): ["
+ << base::Join(known_keys, ", ") << "]\n";
+ return false;
+ }
+
+ bool success = true;
+
+ // Check message fields.
+ auto&& reflection = message.GetReflection();
+ std::vector<const FieldDescriptor*> set_field_descriptors;
+ reflection->ListFields(message, &set_field_descriptors);
+ for (auto&& field_descriptor : set_field_descriptors) {
+ if (field_descriptor->cpp_type() !=
+ FieldDescriptor::CppType::CPPTYPE_MESSAGE) {
+ continue;
+ }
+ if (field_descriptor->is_map()) {
+ continue;
+ }
+
+ const std::string& json_name = GetJsonName(*field_descriptor);
+ const Json::Value& json_value = json[json_name];
+
+ if (field_descriptor->is_repeated()) {
+ auto&& fields =
+ reflection->GetRepeatedFieldRef<Message>(message, field_descriptor);
+
+ if (json_value.type() != Json::ValueType::arrayValue) {
+ *error << base::Join(*path, ".")
+ << ": not a JSON list. This should not happen.\n";
+ success = false;
+ continue;
+ }
+
+ if (json_value.size() != static_cast<size_t>(fields.size())) {
+ *error << base::Join(*path, ".") << ": JSON list has size "
+ << json_value.size() << " but message has size " << fields.size()
+ << ". This should not happen.\n";
+ success = false;
+ continue;
+ }
+
+ std::unique_ptr<Message> scratch_space(fields.NewMessage());
+ for (int i = 0; i < fields.size(); ++i) {
+ path->push_back(json_name + "[" + std::to_string(i) + "]");
+ auto res = AllFieldsAreKnown(fields.Get(i, scratch_space.get()),
+ json_value[i], path, error);
+ path->pop_back();
+ if (!res) {
+ success = false;
+ }
+ }
+ } else {
+ auto&& field = reflection->GetMessage(message, field_descriptor);
+ path->push_back(json_name);
+ auto res = AllFieldsAreKnown(field, json_value, path, error);
+ path->pop_back();
+ if (!res) {
+ success = false;
+ }
+ }
+ }
+ return success;
+}
+
+bool AllFieldsAreKnown(const google::protobuf::Message& message,
+ const std::string& json, std::string* error) {
+ Json::Reader reader;
+ Json::Value value;
+ if (!reader.parse(json, value)) {
+ *error = reader.getFormattedErrorMessages();
+ return false;
+ }
+
+ std::stringstream errorss;
+ std::vector<std::string> json_tree_path{"<root>"};
+ if (!AllFieldsAreKnown(message, value, &json_tree_path, &errorss)) {
+ *error = errorss.str();
+ return false;
+ }
+ return true;
+}
+
+bool EqReformattedJson(const std::string& json,
+ google::protobuf::Message* scratch_space,
+ std::string* error) {
+ Json::Reader reader;
+ Json::Value old_json;
+ if (!reader.parse(json, old_json)) {
+ *error = reader.getFormattedErrorMessages();
+ return false;
+ }
+
+ auto new_json_string = internal::FormatJson(json, scratch_space);
+ if (!new_json_string.ok()) {
+ *error = new_json_string.error();
+ return false;
+ }
+ Json::Value new_json;
+ if (!reader.parse(*new_json_string, new_json)) {
+ *error = reader.getFormattedErrorMessages();
+ return false;
+ }
+
+ if (old_json != new_json) {
+ std::stringstream ss;
+ ss << "Formatted JSON tree does not match source. Possible reasons "
+ "include: \n"
+ "- JSON Integers (without quotes) are matched against 64-bit "
+ "integers in Prototype\n"
+ " (Reformatted integers will now have quotes.) Quote these integers "
+ "in source\n"
+ " JSON or use 32-bit integers instead.\n"
+ "- Enum values are stored as integers in source JSON file. Use enum "
+ "value name \n"
+ " string instead, or change schema field to string / integers.\n"
+ "- JSON keys are re-formatted to be lowerCamelCase. To fix, define "
+ "json_name "
+ "option\n"
+ " for appropriate fields.\n"
+ "\n"
+ "Reformatted JSON is printed below.\n"
+ << Json::StyledWriter().write(new_json);
+ *error = ss.str();
+ return false;
+ }
+ return true;
+}
+
+namespace internal {
+ErrorOr<std::string> FormatJson(const std::string& json,
+ google::protobuf::Message* scratch_space) {
+ auto res = internal::JsonStringToMessage(json, scratch_space);
+ if (!res.ok()) {
+ return MakeError<std::string>(res.error());
+ }
+ return MessageToJsonString(*scratch_space);
+}
+} // namespace internal
+
+} // namespace jsonpb
+} // namespace android