Memoize a map of alternate enum JSON names when parsing JSON in C++. Alternate enum JSON names (aka "EnumValueOption") is a planned feature of Edition 2026. name CYCLES/op CYCLES/op vs base BM_ParseJsonDefault 829.6 ± 1% 844.6 ± 1% +1.80% (p=0.000 n=100) BM_ParseJsonDefaultCaseless 1037.9 ± 0% 889.8 ± 0% -14.27% (n=100) BM_ParseJsonCustom 1048.9 ± 1% 892.6 ± 0% -14.90% (n=100) BM_ParseJsonCustomCaseless 1060.9 ± 1% 915.6 ± 0% -13.70% (n=100) BM_ParseJsonUnknown 1.531k ± 0% 1.191k ± 0% -22.23% (n=100) geomean 1.080k 939.4 -13.00% name INSTRUCTIONS/op INSTRUCTIONS/op vs base BM_ParseJsonDefault 3.400k ± 0% 3.439k ± 0% +1.15% (p=0.000 n=100) BM_ParseJsonDefaultCaseless 4.517k ± 0% 3.629k ± 0% -19.65% (n=100) BM_ParseJsonCustom 4.294k ± 0% 3.597k ± 0% -16.23% (n=100) BM_ParseJsonCustomCaseless 4.443k ± 0% 3.688k ± 0% -16.99% (n=100) BM_ParseJsonUnknown 5.979k ± 0% 4.516k ± 0% -24.46% (n=100) geomean 4.454k 3.756k -15.65% name CACHE-MISSES/op CACHE-MISSES/op vs base BM_ParseJsonDefault 7.000m ± 14% 7.000m ± 14% ~ (p=0.279 n=100) BM_ParseJsonDefaultCaseless 10.000m ± 20% 7.000m ± 14% -30.00% (p=0.002 n=100) BM_ParseJsonCustom 9.500m ± 5% 8.000m ± 12% -15.79% (p=0.000 n=100) BM_ParseJsonCustomCaseless 10.000m ± 10% 8.000m ± 0% -20.00% (p=0.000 n=100) BM_ParseJsonUnknown 18.00m ± 11% 13.00m ± 8% -27.78% (p=0.000 n=100) geomean 10.37m 8.357m -19.38% name BRANCH-MISSES/op BRANCH-MISSES/op vs base BM_ParseJsonDefault 78.00m ± 0% 77.00m ± 0% -1.28% (p=0.000 n=100) BM_ParseJsonDefaultCaseless 90.00m ± 0% 78.00m ± 5% -13.33% (n=100) BM_ParseJsonCustom 96.00m ± 2% 91.00m ± 0% -5.21% (p=0.000 n=100) BM_ParseJsonCustomCaseless 97.00m ± 1% 92.50m ± 1% -4.64% (n=100) BM_ParseJsonUnknown 39.00m ± 3% 27.00m ± 4% -30.77% (n=100) geomean 76.08m 67.15m -11.75% name allocs/op allocs/op vs base BM_ParseJsonDefault 7.001 ± 0% 7.001 ± 0% ~ (p=0.898 n=100) BM_ParseJsonDefaultCaseless 11.144 ± 0% 7.000 ± 0% -37.19% (n=100) BM_ParseJsonCustom 11.144 ± 0% 7.001 ± 0% -37.18% (n=100) BM_ParseJsonCustomCaseless 11.144 ± 0% 7.000 ± 0% -37.19% (n=100) BM_ParseJsonUnknown 24.00 ± 0% 16.00 ± 0% -33.33% (n=100) geomean 11.84 8.259 -30.24% name peak-mem(Bytes)/op peak-mem(Bytes)/op vs base BM_ParseJsonDefault 824.0 ± 6% 824.0 ± 6% ~ (p=1.000 n=100) BM_ParseJsonDefaultCaseless 824.0 ± 6% 776.0 ± 6% ~ (p=0.158 n=100) BM_ParseJsonCustom 824.0 ± 6% 824.0 ± 6% ~ (p=0.321 n=100) BM_ParseJsonCustomCaseless 824.0 ± 6% 776.0 ± 6% ~ (p=0.673 n=100) BM_ParseJsonUnknown 1.000k ± 0% 1.048k ± 0% +4.80% (p=0.000 n=100) geomean 856.5 844.1 -1.45% PiperOrigin-RevId: 939991030
diff --git a/src/google/protobuf/BUILD.bazel b/src/google/protobuf/BUILD.bazel index 9b25d1a..5e2197c 100644 --- a/src/google/protobuf/BUILD.bazel +++ b/src/google/protobuf/BUILD.bazel
@@ -902,6 +902,7 @@ "@abseil-cpp//absl/functional:function_ref", "@abseil-cpp//absl/functional:overload", "@abseil-cpp//absl/hash", + "@abseil-cpp//absl/log", "@abseil-cpp//absl/log:absl_check", "@abseil-cpp//absl/log:absl_log", "@abseil-cpp//absl/memory",
diff --git a/src/google/protobuf/descriptor.h b/src/google/protobuf/descriptor.h index 91aef65..b67ed53 100644 --- a/src/google/protobuf/descriptor.h +++ b/src/google/protobuf/descriptor.h
@@ -32,8 +32,11 @@ #define GOOGLE_PROTOBUF_DESCRIPTOR_H__ #include <atomic> +#include <cstddef> #include <cstdint> +#include <cstring> #include <iterator> +#include <limits> #include <memory> #include <string> #include <type_traits> @@ -49,6 +52,7 @@ #include "absl/functional/function_ref.h" #include "absl/log/absl_check.h" #include "absl/log/absl_log.h" +#include "absl/log/log.h" #include "absl/strings/str_format.h" #include "absl/strings/string_view.h" #include "absl/synchronization/mutex.h" @@ -158,6 +162,10 @@ class ValidationErrorTest; } // namespace descriptor_unittest +namespace json_internal { +struct Proto2Descriptor; +} // namespace json_internal + // Defined in printer.h namespace io { class Printer; @@ -2693,6 +2701,7 @@ friend class TextFormat; friend Reflection; friend class ::google::protobuf::compiler::java::MemoizeProjection; + friend struct ::google::protobuf::json_internal::Proto2Descriptor; struct MemoBase { virtual ~MemoBase() = default;
diff --git a/src/google/protobuf/json/BUILD b/src/google/protobuf/json/BUILD index 4f61fdc..2ab38bc 100644 --- a/src/google/protobuf/json/BUILD +++ b/src/google/protobuf/json/BUILD
@@ -223,6 +223,7 @@ "//src/google/protobuf:port", "//src/google/protobuf/stubs", "@abseil-cpp//absl/algorithm:container", + "@abseil-cpp//absl/container:flat_hash_map", "@abseil-cpp//absl/log:absl_log", "@abseil-cpp//absl/status", "@abseil-cpp//absl/status:statusor",
diff --git a/src/google/protobuf/json/internal/descriptor_traits.h b/src/google/protobuf/json/internal/descriptor_traits.h index 435eda0..e589236 100644 --- a/src/google/protobuf/json/internal/descriptor_traits.h +++ b/src/google/protobuf/json/internal/descriptor_traits.h
@@ -8,22 +8,21 @@ #ifndef GOOGLE_PROTOBUF_JSON_INTERNAL_DESCRIPTOR_TRAITS_H__ #define GOOGLE_PROTOBUF_JSON_INTERNAL_DESCRIPTOR_TRAITS_H__ -#include <array> -#include <cfloat> -#include <cmath> #include <cstdint> #include <cstring> -#include <limits> #include <string> #include <utility> #include "google/protobuf/type.pb.h" #include "google/protobuf/descriptor.pb.h" #include "absl/algorithm/container.h" +#include "absl/container/flat_hash_map.h" #include "absl/log/absl_log.h" #include "absl/status/status.h" #include "absl/status/statusor.h" +#include "absl/strings/ascii.h" #include "absl/strings/match.h" +#include "absl/strings/str_cat.h" #include "absl/strings/str_format.h" #include "absl/strings/string_view.h" #include "absl/types/optional.h" @@ -32,7 +31,6 @@ #include "google/protobuf/json/internal/lexer.h" #include "google/protobuf/json/internal/untyped_message.h" #include "google/protobuf/json_enumvalue_options.pb.h" -#include "google/protobuf/message.h" #include "google/protobuf/stubs/status_macros.h" @@ -256,24 +254,34 @@ static absl::StatusOr<int32_t> EnumNumberByName(Field f, absl::string_view name, bool case_insensitive) { - if (const auto* ev = f->enum_type()->FindValueByName(name)) { + if (const EnumValueDescriptor* ev = f->enum_type()->FindValueByName(name)) { return ev->number(); } - for (int i = 0; i < f->enum_type()->value_count(); ++i) { - const auto* ev = f->enum_type()->value(i); - if (ev->options().HasExtension(pb::enumvalue::json)) { - auto opts = ev->options().GetExtension(pb::enumvalue::json); - if (opts.string() == name || - (case_insensitive && absl::EqualsIgnoreCase(opts.string(), name))) { - return ev->number(); - } + if (case_insensitive) { + // Under case-insensitive mode, we generate a map containing all valid + // lower-case strings of the enum field. + const absl::flat_hash_map<std::string, int32_t>& alternate_names = + DescriptorPool::MemoizeProjection( + f->enum_type(), [](const EnumDescriptor* e) { + return MakeEnumJsonNameMapCaseless(e); + }); + // Similarly, we need to perform a lookup with the enum string transformed + // to lower-case. + auto it = alternate_names.find(absl::AsciiStrToLower(name)); + if (it != alternate_names.end()) { + return it->second; } - if (case_insensitive && absl::EqualsIgnoreCase(name, ev->name())) { - return ev->number(); + } else { + const absl::flat_hash_map<std::string, int32_t>& alternate_names = + DescriptorPool::MemoizeProjection( + f->enum_type(), + [](const EnumDescriptor* e) { return MakeEnumJsonNameMap(e); }); + auto it = alternate_names.find(name); + if (it != alternate_names.end()) { + return it->second; } } - return absl::InvalidArgumentError( absl::StrFormat("unknown enum value: '%s'", name)); } @@ -322,6 +330,40 @@ return body(*dyn_desc); } + + private: + static absl::flat_hash_map<std::string, int32_t> MakeEnumJsonNameMap( + const EnumDescriptor* e) { + absl::flat_hash_map<std::string, int32_t> alternate_names; + + for (int i = 0; i < e->value_count(); ++i) { + const EnumValueDescriptor* ev = e->value(i); + if (ev->options().HasExtension(pb::enumvalue::json)) { + const pb::enumvalue::JsonEnumValueOptions opts = + ev->options().GetExtension(pb::enumvalue::json); + alternate_names[opts.string()] = ev->number(); + } + } + + return alternate_names; + } + + static absl::flat_hash_map<std::string, int32_t> MakeEnumJsonNameMapCaseless( + const EnumDescriptor* e) { + absl::flat_hash_map<std::string, int32_t> alternate_names; + + for (int i = 0; i < e->value_count(); ++i) { + const EnumValueDescriptor* ev = e->value(i); + if (ev->options().HasExtension(pb::enumvalue::json)) { + const pb::enumvalue::JsonEnumValueOptions opts = + ev->options().GetExtension(pb::enumvalue::json); + alternate_names[absl::AsciiStrToLower(opts.string())] = ev->number(); + } + alternate_names[absl::AsciiStrToLower(ev->name())] = ev->number(); + } + + return alternate_names; + } }; // Traits for proto3-ish deserialization.
diff --git a/src/google/protobuf/json/json_enumval_custom_string_test.cc b/src/google/protobuf/json/json_enumval_custom_string_test.cc index e5f5ea4..3da5c88 100644 --- a/src/google/protobuf/json/json_enumval_custom_string_test.cc +++ b/src/google/protobuf/json/json_enumval_custom_string_test.cc
@@ -96,6 +96,20 @@ EXPECT_EQ(msg2.armor(), Armor::ARMOR_GAUNTLET); } +// Test that regular case-insensitive payloads can still be parsed. +TEST(JsonEnumvalCustomStringTest, CaseInsensitiveGauntletRawNameParsing) { + std::string json_res = "{\"armor\":\"armor_GAUNtlet\"}"; + json::ParseOptions parse_options; + parse_options.case_insensitive_enum_parsing = true; + + Knight msg2; + absl::Status parse_status = + json::JsonStringToMessage(json_res, &msg2, parse_options); + + EXPECT_OK(parse_status); + EXPECT_EQ(msg2.armor(), Armor::ARMOR_GAUNTLET); +} + // Test quotes surrounding the custom json enumval. TEST(JsonEnumvalCustomStringTest, DoubleQuoteEnumSerialization) { Knight msg;