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;