Declarative JSON parser (#30442)

* Declarative JSON parser

* Automated change: Fix sanity tests

* fix

* shrinking stuff a little

* static vtables

* separate fns

* simpler?

* make maps work

* windows fixes

* Automated change: Fix sanity tests

* simplify code

* Automated change: Fix sanity tests

* vtable-test

* dont always create vec/map impls for every type

* comments

* make error consistent

* move method private

* progress

* durations!

* Automated change: Fix sanity tests

* fix

* fix

* fix

* Automated change: Fix sanity tests

* post-load

* Automated change: Fix sanity tests

* document JsonPostLoad() and add static_assert

* don't copy field names, to avoid length limitations

* use absl::Status

* accept either string or number for numeric values

* add test for direct data member of another struct type

* remove unused method

* add support for retaining part of the JSON wirthout processing

* update test for changes in Json::Parse() API

* add absl::optional support

* Automated change: Fix sanity tests

* fix tests, improve error messages, and add overload to parse to existing object

* remove overload of LoadFromJson()

* change special case for Json to instead use Json::Object

* fix build

* improve error structure, add missing types, and improve tests

* clang-format

* Automated change: Fix sanity tests

* fix build

* add LoadJsonObjectField(), add LoadFromJson() overload that takes an ErrorList parameter, and add tests for parsing bare top-level types

* fix msan

* Automated change: Fix sanity tests

* fix error message

* Automated change: Fix sanity tests

* add mechanism to conditionally disable individual fields

* fix build

Co-authored-by: Craig Tiller <craig.tiller@gmail.com>
Co-authored-by: ctiller <ctiller@users.noreply.github.com>
Co-authored-by: Craig Tiller <ctiller@google.com>
Co-authored-by: markdroth <markdroth@users.noreply.github.com>
diff --git a/BUILD b/BUILD
index 88a3d15..3541025 100644
--- a/BUILD
+++ b/BUILD
@@ -7260,10 +7260,52 @@
         "error",
         "gpr_base",
         "json",
+        "json_args",
+        "json_object_loader",
         "time",
     ],
 )
 
+grpc_cc_library(
+    name = "json_args",
+    hdrs = ["src/core/lib/json/json_args.h"],
+    external_deps = ["absl/strings"],
+    deps = ["gpr_base"],
+)
+
+grpc_cc_library(
+    name = "json_object_loader",
+    srcs = ["src/core/lib/json/json_object_loader.cc"],
+    hdrs = ["src/core/lib/json/json_object_loader.h"],
+    external_deps = [
+        "absl/meta:type_traits",
+        "absl/status",
+        "absl/status:statusor",
+        "absl/strings",
+        "absl/types:optional",
+    ],
+    deps = [
+        "gpr_base",
+        "json",
+        "json_args",
+        "time",
+    ],
+)
+
+grpc_cc_library(
+    name = "json_channel_args",
+    hdrs = ["src/core/lib/json/json_channel_args.h"],
+    external_deps = [
+        "absl/strings",
+        "absl/types:optional",
+    ],
+    deps = [
+        "channel_args",
+        "gpr",
+        "json_args",
+    ],
+)
+
 ### UPB Targets
 
 grpc_upb_proto_library(
diff --git a/CMakeLists.txt b/CMakeLists.txt
index e02b878..2f374f2 100644
--- a/CMakeLists.txt
+++ b/CMakeLists.txt
@@ -1025,6 +1025,7 @@
   endif()
   add_dependencies(buildtests_cxx istio_echo_server_test)
   add_dependencies(buildtests_cxx join_test)
+  add_dependencies(buildtests_cxx json_object_loader_test)
   add_dependencies(buildtests_cxx json_test)
   add_dependencies(buildtests_cxx json_token_test)
   add_dependencies(buildtests_cxx jwt_verifier_test)
@@ -2186,6 +2187,7 @@
   src/core/lib/iomgr/wakeup_fd_nospecial.cc
   src/core/lib/iomgr/wakeup_fd_pipe.cc
   src/core/lib/iomgr/wakeup_fd_posix.cc
+  src/core/lib/json/json_object_loader.cc
   src/core/lib/json/json_reader.cc
   src/core/lib/json/json_util.cc
   src/core/lib/json/json_writer.cc
@@ -2794,6 +2796,7 @@
   src/core/lib/iomgr/wakeup_fd_nospecial.cc
   src/core/lib/iomgr/wakeup_fd_pipe.cc
   src/core/lib/iomgr/wakeup_fd_posix.cc
+  src/core/lib/json/json_object_loader.cc
   src/core/lib/json/json_reader.cc
   src/core/lib/json/json_util.cc
   src/core/lib/json/json_writer.cc
@@ -12626,6 +12629,41 @@
 endif()
 if(gRPC_BUILD_TESTS)
 
+add_executable(json_object_loader_test
+  test/core/json/json_object_loader_test.cc
+  third_party/googletest/googletest/src/gtest-all.cc
+  third_party/googletest/googlemock/src/gmock-all.cc
+)
+
+target_include_directories(json_object_loader_test
+  PRIVATE
+    ${CMAKE_CURRENT_SOURCE_DIR}
+    ${CMAKE_CURRENT_SOURCE_DIR}/include
+    ${_gRPC_ADDRESS_SORTING_INCLUDE_DIR}
+    ${_gRPC_RE2_INCLUDE_DIR}
+    ${_gRPC_SSL_INCLUDE_DIR}
+    ${_gRPC_UPB_GENERATED_DIR}
+    ${_gRPC_UPB_GRPC_GENERATED_DIR}
+    ${_gRPC_UPB_INCLUDE_DIR}
+    ${_gRPC_XXHASH_INCLUDE_DIR}
+    ${_gRPC_ZLIB_INCLUDE_DIR}
+    third_party/googletest/googletest/include
+    third_party/googletest/googletest
+    third_party/googletest/googlemock/include
+    third_party/googletest/googlemock
+    ${_gRPC_PROTO_GENS_DIR}
+)
+
+target_link_libraries(json_object_loader_test
+  ${_gRPC_PROTOBUF_LIBRARIES}
+  ${_gRPC_ALLTARGETS_LIBRARIES}
+  grpc_test_util
+)
+
+
+endif()
+if(gRPC_BUILD_TESTS)
+
 add_executable(json_test
   test/core/json/json_test.cc
   third_party/googletest/googletest/src/gtest-all.cc
diff --git a/Makefile b/Makefile
index cf54b0c..9c902fc 100644
--- a/Makefile
+++ b/Makefile
@@ -1548,6 +1548,7 @@
     src/core/lib/iomgr/wakeup_fd_nospecial.cc \
     src/core/lib/iomgr/wakeup_fd_pipe.cc \
     src/core/lib/iomgr/wakeup_fd_posix.cc \
+    src/core/lib/json/json_object_loader.cc \
     src/core/lib/json/json_reader.cc \
     src/core/lib/json/json_util.cc \
     src/core/lib/json/json_writer.cc \
@@ -2020,6 +2021,7 @@
     src/core/lib/iomgr/wakeup_fd_nospecial.cc \
     src/core/lib/iomgr/wakeup_fd_pipe.cc \
     src/core/lib/iomgr/wakeup_fd_posix.cc \
+    src/core/lib/json/json_object_loader.cc \
     src/core/lib/json/json_reader.cc \
     src/core/lib/json/json_util.cc \
     src/core/lib/json/json_writer.cc \
diff --git a/build_autogenerated.yaml b/build_autogenerated.yaml
index 049d1f3..42686e3 100644
--- a/build_autogenerated.yaml
+++ b/build_autogenerated.yaml
@@ -849,6 +849,8 @@
   - src/core/lib/iomgr/wakeup_fd_pipe.h
   - src/core/lib/iomgr/wakeup_fd_posix.h
   - src/core/lib/json/json.h
+  - src/core/lib/json/json_args.h
+  - src/core/lib/json/json_object_loader.h
   - src/core/lib/json/json_util.h
   - src/core/lib/load_balancing/lb_policy.h
   - src/core/lib/load_balancing/lb_policy_factory.h
@@ -1540,6 +1542,7 @@
   - src/core/lib/iomgr/wakeup_fd_nospecial.cc
   - src/core/lib/iomgr/wakeup_fd_pipe.cc
   - src/core/lib/iomgr/wakeup_fd_posix.cc
+  - src/core/lib/json/json_object_loader.cc
   - src/core/lib/json/json_reader.cc
   - src/core/lib/json/json_util.cc
   - src/core/lib/json/json_writer.cc
@@ -2031,6 +2034,8 @@
   - src/core/lib/iomgr/wakeup_fd_pipe.h
   - src/core/lib/iomgr/wakeup_fd_posix.h
   - src/core/lib/json/json.h
+  - src/core/lib/json/json_args.h
+  - src/core/lib/json/json_object_loader.h
   - src/core/lib/json/json_util.h
   - src/core/lib/load_balancing/lb_policy.h
   - src/core/lib/load_balancing/lb_policy_factory.h
@@ -2363,6 +2368,7 @@
   - src/core/lib/iomgr/wakeup_fd_nospecial.cc
   - src/core/lib/iomgr/wakeup_fd_pipe.cc
   - src/core/lib/iomgr/wakeup_fd_posix.cc
+  - src/core/lib/json/json_object_loader.cc
   - src/core/lib/json/json_reader.cc
   - src/core/lib/json/json_util.cc
   - src/core/lib/json/json_writer.cc
@@ -7392,6 +7398,16 @@
   - absl/types:variant
   - absl/utility:utility
   uses_polling: false
+- name: json_object_loader_test
+  gtest: true
+  build: test
+  language: c++
+  headers: []
+  src:
+  - test/core/json/json_object_loader_test.cc
+  deps:
+  - grpc_test_util
+  uses_polling: false
 - name: json_test
   gtest: true
   build: test
diff --git a/config.m4 b/config.m4
index 6dd509e..06d3581 100644
--- a/config.m4
+++ b/config.m4
@@ -607,6 +607,7 @@
     src/core/lib/iomgr/wakeup_fd_nospecial.cc \
     src/core/lib/iomgr/wakeup_fd_pipe.cc \
     src/core/lib/iomgr/wakeup_fd_posix.cc \
+    src/core/lib/json/json_object_loader.cc \
     src/core/lib/json/json_reader.cc \
     src/core/lib/json/json_util.cc \
     src/core/lib/json/json_writer.cc \
diff --git a/config.w32 b/config.w32
index 46cfab9..26f612c 100644
--- a/config.w32
+++ b/config.w32
@@ -573,6 +573,7 @@
     "src\\core\\lib\\iomgr\\wakeup_fd_nospecial.cc " +
     "src\\core\\lib\\iomgr\\wakeup_fd_pipe.cc " +
     "src\\core\\lib\\iomgr\\wakeup_fd_posix.cc " +
+    "src\\core\\lib\\json\\json_object_loader.cc " +
     "src\\core\\lib\\json\\json_reader.cc " +
     "src\\core\\lib\\json\\json_util.cc " +
     "src\\core\\lib\\json\\json_writer.cc " +
diff --git a/gRPC-C++.podspec b/gRPC-C++.podspec
index fb0d095..59c6c1e 100644
--- a/gRPC-C++.podspec
+++ b/gRPC-C++.podspec
@@ -807,6 +807,8 @@
                       'src/core/lib/iomgr/wakeup_fd_pipe.h',
                       'src/core/lib/iomgr/wakeup_fd_posix.h',
                       'src/core/lib/json/json.h',
+                      'src/core/lib/json/json_args.h',
+                      'src/core/lib/json/json_object_loader.h',
                       'src/core/lib/json/json_util.h',
                       'src/core/lib/load_balancing/lb_policy.h',
                       'src/core/lib/load_balancing/lb_policy_factory.h',
@@ -1659,6 +1661,8 @@
                               'src/core/lib/iomgr/wakeup_fd_pipe.h',
                               'src/core/lib/iomgr/wakeup_fd_posix.h',
                               'src/core/lib/json/json.h',
+                              'src/core/lib/json/json_args.h',
+                              'src/core/lib/json/json_object_loader.h',
                               'src/core/lib/json/json_util.h',
                               'src/core/lib/load_balancing/lb_policy.h',
                               'src/core/lib/load_balancing/lb_policy_factory.h',
diff --git a/gRPC-Core.podspec b/gRPC-Core.podspec
index 456bd60..7b998c9 100644
--- a/gRPC-Core.podspec
+++ b/gRPC-Core.podspec
@@ -1312,6 +1312,9 @@
                       'src/core/lib/iomgr/wakeup_fd_posix.cc',
                       'src/core/lib/iomgr/wakeup_fd_posix.h',
                       'src/core/lib/json/json.h',
+                      'src/core/lib/json/json_args.h',
+                      'src/core/lib/json/json_object_loader.cc',
+                      'src/core/lib/json/json_object_loader.h',
                       'src/core/lib/json/json_reader.cc',
                       'src/core/lib/json/json_util.cc',
                       'src/core/lib/json/json_util.h',
@@ -2280,6 +2283,8 @@
                               'src/core/lib/iomgr/wakeup_fd_pipe.h',
                               'src/core/lib/iomgr/wakeup_fd_posix.h',
                               'src/core/lib/json/json.h',
+                              'src/core/lib/json/json_args.h',
+                              'src/core/lib/json/json_object_loader.h',
                               'src/core/lib/json/json_util.h',
                               'src/core/lib/load_balancing/lb_policy.h',
                               'src/core/lib/load_balancing/lb_policy_factory.h',
diff --git a/grpc.gemspec b/grpc.gemspec
index 5215d8c..3e09b77 100644
--- a/grpc.gemspec
+++ b/grpc.gemspec
@@ -1225,6 +1225,9 @@
   s.files += %w( src/core/lib/iomgr/wakeup_fd_posix.cc )
   s.files += %w( src/core/lib/iomgr/wakeup_fd_posix.h )
   s.files += %w( src/core/lib/json/json.h )
+  s.files += %w( src/core/lib/json/json_args.h )
+  s.files += %w( src/core/lib/json/json_object_loader.cc )
+  s.files += %w( src/core/lib/json/json_object_loader.h )
   s.files += %w( src/core/lib/json/json_reader.cc )
   s.files += %w( src/core/lib/json/json_util.cc )
   s.files += %w( src/core/lib/json/json_util.h )
diff --git a/grpc.gyp b/grpc.gyp
index a9b7fae..332a579 100644
--- a/grpc.gyp
+++ b/grpc.gyp
@@ -899,6 +899,7 @@
         'src/core/lib/iomgr/wakeup_fd_nospecial.cc',
         'src/core/lib/iomgr/wakeup_fd_pipe.cc',
         'src/core/lib/iomgr/wakeup_fd_posix.cc',
+        'src/core/lib/json/json_object_loader.cc',
         'src/core/lib/json/json_reader.cc',
         'src/core/lib/json/json_util.cc',
         'src/core/lib/json/json_writer.cc',
@@ -1339,6 +1340,7 @@
         'src/core/lib/iomgr/wakeup_fd_nospecial.cc',
         'src/core/lib/iomgr/wakeup_fd_pipe.cc',
         'src/core/lib/iomgr/wakeup_fd_posix.cc',
+        'src/core/lib/json/json_object_loader.cc',
         'src/core/lib/json/json_reader.cc',
         'src/core/lib/json/json_util.cc',
         'src/core/lib/json/json_writer.cc',
diff --git a/package.xml b/package.xml
index e8547b3..2a61574 100644
--- a/package.xml
+++ b/package.xml
@@ -1207,6 +1207,9 @@
     <file baseinstalldir="/" name="src/core/lib/iomgr/wakeup_fd_posix.cc" role="src" />
     <file baseinstalldir="/" name="src/core/lib/iomgr/wakeup_fd_posix.h" role="src" />
     <file baseinstalldir="/" name="src/core/lib/json/json.h" role="src" />
+    <file baseinstalldir="/" name="src/core/lib/json/json_args.h" role="src" />
+    <file baseinstalldir="/" name="src/core/lib/json/json_object_loader.cc" role="src" />
+    <file baseinstalldir="/" name="src/core/lib/json/json_object_loader.h" role="src" />
     <file baseinstalldir="/" name="src/core/lib/json/json_reader.cc" role="src" />
     <file baseinstalldir="/" name="src/core/lib/json/json_util.cc" role="src" />
     <file baseinstalldir="/" name="src/core/lib/json/json_util.h" role="src" />
diff --git a/src/core/lib/json/json_args.h b/src/core/lib/json/json_args.h
new file mode 100644
index 0000000..e975d33
--- /dev/null
+++ b/src/core/lib/json/json_args.h
@@ -0,0 +1,34 @@
+// Copyright 2020 gRPC authors.
+//
+// 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.
+
+#ifndef GRPC_CORE_LIB_JSON_JSON_ARGS_H
+#define GRPC_CORE_LIB_JSON_JSON_ARGS_H
+
+#include <grpc/support/port_platform.h>
+
+#include "absl/strings/string_view.h"
+
+namespace grpc_core {
+
+class JsonArgs {
+ public:
+  JsonArgs() = default;
+  virtual ~JsonArgs() = default;
+
+  virtual bool IsEnabled(absl::string_view /*key*/) const { return true; }
+};
+
+}  // namespace grpc_core
+
+#endif  // GRPC_CORE_LIB_JSON_JSON_ARGS_H
diff --git a/src/core/lib/json/json_channel_args.h b/src/core/lib/json/json_channel_args.h
new file mode 100644
index 0000000..668cb4a
--- /dev/null
+++ b/src/core/lib/json/json_channel_args.h
@@ -0,0 +1,42 @@
+// Copyright 2022 gRPC authors.
+//
+// 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.
+
+#ifndef GRPC_CORE_LIB_JSON_JSON_CHANNEL_ARGS_H
+#define GRPC_CORE_LIB_JSON_JSON_CHANNEL_ARGS_H
+
+#include <grpc/support/port_platform.h>
+
+#include "absl/strings/string_view.h"
+#include "absl/types/optional.h"
+
+#include "src/core/lib/channel/channel_args.h"
+#include "src/core/lib/json/json_args.h"
+
+namespace grpc_core {
+
+class JsonChannelArgs : public JsonArgs {
+ public:
+  explicit JsonChannelArgs(const ChannelArgs& args) : args_(args) {}
+
+  bool IsEnabled(absl::string_view key) const override {
+    return args_.GetBool(key).value_or(false);
+  }
+
+ private:
+  ChannelArgs args_;
+};
+
+}  // namespace grpc_core
+
+#endif  // GRPC_CORE_LIB_JSON_JSON_CHANNEL_ARGS_H
diff --git a/src/core/lib/json/json_object_loader.cc b/src/core/lib/json/json_object_loader.cc
new file mode 100644
index 0000000..ae32710
--- /dev/null
+++ b/src/core/lib/json/json_object_loader.cc
@@ -0,0 +1,204 @@
+// Copyright 2020 gRPC authors.
+//
+// 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 <grpc/support/port_platform.h>
+
+#include "src/core/lib/json/json_object_loader.h"
+
+#include <algorithm>
+#include <utility>
+
+#include "absl/status/status.h"
+#include "absl/strings/ascii.h"
+#include "absl/strings/str_cat.h"
+#include "absl/strings/str_join.h"
+#include "absl/strings/strip.h"
+
+namespace grpc_core {
+
+void ErrorList::PushField(absl::string_view ext) {
+  // Skip leading '.' for top-level field names.
+  if (fields_.empty()) absl::ConsumePrefix(&ext, ".");
+  fields_.emplace_back(std::string(ext));
+}
+
+void ErrorList::PopField() { fields_.pop_back(); }
+
+void ErrorList::AddError(absl::string_view error) {
+  field_errors_[absl::StrJoin(fields_, "")].emplace_back(error);
+}
+
+bool ErrorList::FieldHasErrors() const {
+  return field_errors_.find(absl::StrJoin(fields_, "")) != field_errors_.end();
+}
+
+absl::Status ErrorList::status() const {
+  if (field_errors_.empty()) return absl::OkStatus();
+  std::vector<std::string> errors;
+  for (const auto& p : field_errors_) {
+    if (p.second.size() > 1) {
+      errors.emplace_back(absl::StrCat("field:", p.first, " errors:[",
+                                       absl::StrJoin(p.second, "; "), "]"));
+    } else {
+      errors.emplace_back(
+          absl::StrCat("field:", p.first, " error:", p.second[0]));
+    }
+  }
+  return absl::InvalidArgumentError(absl::StrCat(
+      "errors validating JSON: [", absl::StrJoin(errors, "; "), "]"));
+}
+
+namespace json_detail {
+
+void LoadScalar::LoadInto(const Json& json, const JsonArgs& /*args*/, void* dst,
+                          ErrorList* errors) const {
+  // We accept either STRING or NUMBER for numeric values, as per
+  // https://developers.google.com/protocol-buffers/docs/proto3#json.
+  if (json.type() != Json::Type::STRING &&
+      (!IsNumber() || json.type() != Json::Type::NUMBER)) {
+    errors->AddError(
+        absl::StrCat("is not a ", IsNumber() ? "number" : "string"));
+    return;
+  }
+  return LoadInto(json.string_value(), dst, errors);
+}
+
+bool LoadString::IsNumber() const { return false; }
+
+void LoadString::LoadInto(const std::string& value, void* dst,
+                          ErrorList*) const {
+  *static_cast<std::string*>(dst) = value;
+}
+
+bool LoadDuration::IsNumber() const { return false; }
+
+void LoadDuration::LoadInto(const std::string& value, void* dst,
+                            ErrorList* errors) const {
+  absl::string_view buf(value);
+  if (!absl::ConsumeSuffix(&buf, "s")) {
+    errors->AddError("Not a duration (no s suffix)");
+    return;
+  }
+  buf = absl::StripAsciiWhitespace(buf);
+  auto decimal_point = buf.find('.');
+  int nanos = 0;
+  if (decimal_point != absl::string_view::npos) {
+    absl::string_view after_decimal = buf.substr(decimal_point + 1);
+    buf = buf.substr(0, decimal_point);
+    if (!absl::SimpleAtoi(after_decimal, &nanos)) {
+      errors->AddError("Not a duration (not a number of nanoseconds)");
+      return;
+    }
+    if (after_decimal.length() > 9) {
+      // We don't accept greater precision than nanos.
+      errors->AddError("Not a duration (too many digits after decimal)");
+      return;
+    }
+    for (size_t i = 0; i < (9 - after_decimal.length()); ++i) {
+      nanos *= 10;
+    }
+  }
+  int seconds;
+  if (!absl::SimpleAtoi(buf, &seconds)) {
+    errors->AddError("Not a duration (not a number of seconds)");
+    return;
+  }
+  *static_cast<Duration*>(dst) =
+      Duration::FromSecondsAndNanoseconds(seconds, nanos);
+}
+
+bool LoadNumber::IsNumber() const { return true; }
+
+void LoadBool::LoadInto(const Json& json, const JsonArgs&, void* dst,
+                        ErrorList* errors) const {
+  if (json.type() == Json::Type::JSON_TRUE) {
+    *static_cast<bool*>(dst) = true;
+  } else if (json.type() == Json::Type::JSON_FALSE) {
+    *static_cast<bool*>(dst) = false;
+  } else {
+    errors->AddError("is not a boolean");
+  }
+}
+
+void LoadUnprocessedJsonObject::LoadInto(const Json& json, const JsonArgs&,
+                                         void* dst, ErrorList* errors) const {
+  if (json.type() != Json::Type::OBJECT) {
+    errors->AddError("is not an object");
+    return;
+  }
+  *static_cast<Json::Object*>(dst) = json.object_value();
+}
+
+void LoadVector::LoadInto(const Json& json, const JsonArgs& args, void* dst,
+                          ErrorList* errors) const {
+  if (json.type() != Json::Type::ARRAY) {
+    errors->AddError("is not an array");
+    return;
+  }
+  const auto& array = json.array_value();
+  for (size_t i = 0; i < array.size(); ++i) {
+    ScopedField field(errors, absl::StrCat("[", i, "]"));
+    LoadOne(array[i], args, dst, errors);
+  }
+}
+
+void LoadMap::LoadInto(const Json& json, const JsonArgs& args, void* dst,
+                       ErrorList* errors) const {
+  if (json.type() != Json::Type::OBJECT) {
+    errors->AddError("is not an object");
+    return;
+  }
+  for (const auto& pair : json.object_value()) {
+    ScopedField field(errors, absl::StrCat("[\"", pair.first, "\"]"));
+    LoadOne(pair.second, args, pair.first, dst, errors);
+  }
+}
+
+bool LoadObject(const Json& json, const JsonArgs& args, const Element* elements,
+                size_t num_elements, void* dst, ErrorList* errors) {
+  if (json.type() != Json::Type::OBJECT) {
+    errors->AddError("is not an object");
+    return false;
+  }
+  for (size_t i = 0; i < num_elements; ++i) {
+    const Element& element = elements[i];
+    if (element.enable_key != nullptr && !args.IsEnabled(element.enable_key)) {
+      continue;
+    }
+    ScopedField field(errors, absl::StrCat(".", element.name));
+    const auto& it = json.object_value().find(element.name);
+    if (it == json.object_value().end()) {
+      if (element.optional) continue;
+      errors->AddError("field not present");
+      continue;
+    }
+    char* field_dst = static_cast<char*>(dst) + element.member_offset;
+    element.loader->LoadInto(it->second, args, field_dst, errors);
+  }
+  return true;
+}
+
+const Json* GetJsonObjectField(const Json::Object& json,
+                               absl::string_view field, ErrorList* errors,
+                               bool required) {
+  auto it = json.find(std::string(field));
+  if (it == json.end()) {
+    if (required) errors->AddError("field not present");
+    return nullptr;
+  }
+  return &it->second;
+}
+
+}  // namespace json_detail
+}  // namespace grpc_core
diff --git a/src/core/lib/json/json_object_loader.h b/src/core/lib/json/json_object_loader.h
new file mode 100644
index 0000000..6ccdc2e
--- /dev/null
+++ b/src/core/lib/json/json_object_loader.h
@@ -0,0 +1,544 @@
+// Copyright 2020 gRPC authors.
+//
+// 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.
+
+#ifndef GRPC_CORE_LIB_JSON_JSON_OBJECT_LOADER_H
+#define GRPC_CORE_LIB_JSON_JSON_OBJECT_LOADER_H
+
+#include <grpc/support/port_platform.h>
+
+#include <cstdint>
+#include <cstring>
+#include <map>
+#include <string>
+#include <vector>
+
+#include "absl/meta/type_traits.h"
+#include "absl/status/status.h"
+#include "absl/status/statusor.h"
+#include "absl/strings/numbers.h"
+#include "absl/strings/str_cat.h"
+#include "absl/strings/string_view.h"
+#include "absl/types/optional.h"
+
+#include "src/core/lib/gprpp/time.h"
+#include "src/core/lib/json/json.h"
+#include "src/core/lib/json/json_args.h"
+
+// Provides a means to load JSON objects into C++ objects, with the aim of
+// minimizing object code size.
+//
+// Usage:
+// Given struct Foo:
+//   struct Foo {
+//     int a;
+//     int b;
+//   };
+// We add a static JsonLoader() method to Foo to declare how to load the
+// object from JSON, and an optional JsonPostLoad() method to do any
+// necessary post-processing:
+//   struct Foo {
+//     int a;
+//     int b;
+//     static const JsonLoaderInterface* JsonLoader() {
+//       // Note: Field names must be string constants; they are not copied.
+//       static const auto* loader = JsonObjectLoader<Foo>()
+//           .Field("a", &Foo::a)
+//           .Field("b", &Foo::b)
+//           .Finish();
+//       return loader;
+//     }
+//     // Optional; omit if no post-processing needed.
+//     void JsonPostLoad(const Json& source, ErrorList* errors) { ++a; }
+//   };
+// Now we can load Foo objects from JSON:
+//   absl::StatusOr<Foo> foo = LoadFromJson<Foo>(json);
+namespace grpc_core {
+
+// A list of errors that occurred during JSON parsing.
+// If a non-empty list occurs during parsing, the parsing failed.
+class ErrorList {
+ public:
+  // Record that we're reading some field.
+  void PushField(absl::string_view ext) GPR_ATTRIBUTE_NOINLINE;
+  // Record that we've finished reading that field.
+  void PopField() GPR_ATTRIBUTE_NOINLINE;
+
+  // Record that we've encountered an error.
+  void AddError(absl::string_view error) GPR_ATTRIBUTE_NOINLINE;
+  // Returns true if the current field has errors.
+  bool FieldHasErrors() const GPR_ATTRIBUTE_NOINLINE;
+
+  // Returns the resulting status of parsing.
+  absl::Status status() const;
+
+  // Return true if there are no errors.
+  bool ok() const { return field_errors_.empty(); }
+
+  size_t size() const { return field_errors_.size(); }
+
+ private:
+  // TODO(roth): If we don't actually have any fields for which we
+  // report more than one error, simplify this data structure.
+  std::map<std::string /*field_name*/, std::vector<std::string>> field_errors_;
+  std::vector<std::string> fields_;
+};
+
+// Note that we're reading a field, and remove it at the end of the scope.
+class ScopedField {
+ public:
+  ScopedField(ErrorList* error_list, absl::string_view field_name)
+      : error_list_(error_list) {
+    error_list_->PushField(field_name);
+  }
+  ~ScopedField() { error_list_->PopField(); }
+
+ private:
+  ErrorList* error_list_;
+};
+
+namespace json_detail {
+
+// An un-typed JSON loader.
+class LoaderInterface {
+ public:
+  // Convert json value to whatever type we're loading at dst.
+  // If errors occur, add them to error_list.
+  virtual void LoadInto(const Json& json, const JsonArgs& args, void* dst,
+                        ErrorList* errors) const = 0;
+
+ protected:
+  virtual ~LoaderInterface() = default;
+};
+
+// Loads a scalar (string or number).
+class LoadScalar : public LoaderInterface {
+ public:
+  void LoadInto(const Json& json, const JsonArgs& args, void* dst,
+                ErrorList* errors) const override;
+
+ protected:
+  ~LoadScalar() override = default;
+
+ private:
+  // true if we're loading a number, false if we're loading a string.
+  // We use a virtual function to store this decision in a vtable instead of
+  // needing an instance variable.
+  virtual bool IsNumber() const = 0;
+
+  virtual void LoadInto(const std::string& json, void* dst,
+                        ErrorList* errors) const = 0;
+};
+
+// Load a string.
+class LoadString : public LoadScalar {
+ protected:
+  ~LoadString() override = default;
+
+ private:
+  bool IsNumber() const override;
+  void LoadInto(const std::string& value, void* dst,
+                ErrorList* errors) const override;
+};
+
+// Load a Duration.
+class LoadDuration : public LoadScalar {
+ protected:
+  ~LoadDuration() override = default;
+
+ private:
+  bool IsNumber() const override;
+  void LoadInto(const std::string& value, void* dst,
+                ErrorList* errors) const override;
+};
+
+// Load a number.
+class LoadNumber : public LoadScalar {
+ protected:
+  ~LoadNumber() override = default;
+
+ private:
+  bool IsNumber() const override;
+};
+
+// Load a signed number of type T.
+template <typename T>
+class TypedLoadSignedNumber : public LoadNumber {
+ protected:
+  ~TypedLoadSignedNumber() override = default;
+
+ private:
+  void LoadInto(const std::string& value, void* dst,
+                ErrorList* errors) const override {
+    if (!absl::SimpleAtoi(value, static_cast<T*>(dst))) {
+      errors->AddError("failed to parse number");
+    }
+  }
+};
+
+// Load an unsigned number of type T.
+template <typename T>
+class TypedLoadUnsignedNumber : public LoadNumber {
+ protected:
+  ~TypedLoadUnsignedNumber() override = default;
+
+ private:
+  void LoadInto(const std::string& value, void* dst,
+                ErrorList* errors) const override {
+    if (!absl::SimpleAtoi(value, static_cast<T*>(dst))) {
+      errors->AddError("failed to parse non-negative number");
+    }
+  }
+};
+
+// Load a float.
+class LoadFloat : public LoadNumber {
+ protected:
+  ~LoadFloat() override = default;
+
+ private:
+  void LoadInto(const std::string& value, void* dst,
+                ErrorList* errors) const override {
+    if (!absl::SimpleAtof(value, static_cast<float*>(dst))) {
+      errors->AddError("failed to parse floating-point number");
+    }
+  }
+};
+
+// Load a double.
+class LoadDouble : public LoadNumber {
+ protected:
+  ~LoadDouble() override = default;
+
+ private:
+  void LoadInto(const std::string& value, void* dst,
+                ErrorList* errors) const override {
+    if (!absl::SimpleAtod(value, static_cast<double*>(dst))) {
+      errors->AddError("failed to parse floating-point number");
+    }
+  }
+};
+
+// Load a bool.
+class LoadBool : public LoaderInterface {
+ public:
+  void LoadInto(const Json& json, const JsonArgs& /*args*/, void* dst,
+                ErrorList* errors) const override;
+};
+
+// Loads an unprocessed JSON object value.
+class LoadUnprocessedJsonObject : public LoaderInterface {
+ public:
+  void LoadInto(const Json& json, const JsonArgs& /*args*/, void* dst,
+                ErrorList* errors) const override;
+};
+
+// Load a vector of some type.
+class LoadVector : public LoaderInterface {
+ public:
+  void LoadInto(const Json& json, const JsonArgs& args, void* dst,
+                ErrorList* errors) const override;
+
+ protected:
+  ~LoadVector() override = default;
+
+ private:
+  virtual void LoadOne(const Json& json, const JsonArgs& args, void* dst,
+                       ErrorList* errors) const = 0;
+};
+
+// Load a map of string->some type.
+class LoadMap : public LoaderInterface {
+ public:
+  void LoadInto(const Json& json, const JsonArgs& args, void* dst,
+                ErrorList* errors) const override;
+
+ protected:
+  ~LoadMap() override = default;
+
+ private:
+  virtual void LoadOne(const Json& json, const JsonArgs& args,
+                       const std::string& name, void* dst,
+                       ErrorList* errors) const = 0;
+};
+
+// Fetch a LoaderInterface for some type.
+template <typename T>
+const LoaderInterface* LoaderForType();
+
+// AutoLoader implements LoaderInterface for a type.
+// The default asks the type for its LoaderInterface and then uses that.
+// Classes that load from objects should provide a:
+// static const JsonLoaderInterface* JsonLoader();
+template <typename T>
+class AutoLoader final : public LoaderInterface {
+ public:
+  void LoadInto(const Json& json, const JsonArgs& args, void* dst,
+                ErrorList* errors) const override {
+    T::JsonLoader(args)->LoadInto(json, args, dst, errors);
+  }
+};
+
+// Specializations of AutoLoader for basic types.
+template <>
+class AutoLoader<std::string> final : public LoadString {};
+template <>
+class AutoLoader<Duration> final : public LoadDuration {};
+template <>
+class AutoLoader<int32_t> final : public TypedLoadSignedNumber<int32_t> {};
+template <>
+class AutoLoader<int64_t> final : public TypedLoadSignedNumber<int64_t> {};
+template <>
+class AutoLoader<uint32_t> final : public TypedLoadUnsignedNumber<uint32_t> {};
+template <>
+class AutoLoader<uint64_t> final : public TypedLoadUnsignedNumber<uint64_t> {};
+template <>
+class AutoLoader<float> final : public LoadFloat {};
+template <>
+class AutoLoader<double> final : public LoadDouble {};
+template <>
+class AutoLoader<bool> final : public LoadBool {};
+template <>
+class AutoLoader<Json::Object> final : public LoadUnprocessedJsonObject {};
+
+// Specializations of AutoLoader for vectors.
+template <typename T>
+class AutoLoader<std::vector<T>> final : public LoadVector {
+ private:
+  void LoadOne(const Json& json, const JsonArgs& args, void* dst,
+               ErrorList* errors) const final {
+    auto* vec = static_cast<std::vector<T>*>(dst);
+    T value{};
+    LoaderForType<T>()->LoadInto(json, args, &value, errors);
+    vec->push_back(std::move(value));
+  }
+};
+
+// Specializations of AutoLoader for maps.
+template <typename T>
+class AutoLoader<std::map<std::string, T>> final : public LoadMap {
+ private:
+  void LoadOne(const Json& json, const JsonArgs& args, const std::string& name,
+               void* dst, ErrorList* errors) const final {
+    auto* map = static_cast<std::map<std::string, T>*>(dst);
+    T value{};
+    LoaderForType<T>()->LoadInto(json, args, &value, errors);
+    map->emplace(name, std::move(value));
+  }
+};
+
+// Specializations of AutoLoader for absl::optional<>.
+template <typename T>
+class AutoLoader<absl::optional<T>> final : public LoaderInterface {
+ public:
+  void LoadInto(const Json& json, const JsonArgs& args, void* dst,
+                ErrorList* errors) const override {
+    if (json.type() == Json::Type::JSON_NULL) return;
+    auto* opt = static_cast<absl::optional<T>*>(dst);
+    opt->emplace();
+    LoaderForType<T>()->LoadInto(json, args, &**opt, errors);
+  }
+};
+
+// Implementation of aforementioned LoaderForType.
+// Simply keeps a static AutoLoader<T> and returns a pointer to that.
+template <typename T>
+const LoaderInterface* LoaderForType() {
+  static const auto* loader = new AutoLoader<T>();
+  return loader;
+}
+
+// Element describes one typed field to be loaded from a JSON object.
+struct Element {
+  Element() = default;
+  template <typename A, typename B>
+  Element(const char* name, bool optional, B A::*p,
+          const LoaderInterface* loader, const char* enable_key)
+      : loader(loader),
+        member_offset(static_cast<uint16_t>(
+            reinterpret_cast<uintptr_t>(&(static_cast<A*>(nullptr)->*p)))),
+        optional(optional),
+        name(name),
+        enable_key(enable_key) {}
+  // The loader for this field.
+  const LoaderInterface* loader;
+  // Offset into the destination object to store the field.
+  uint16_t member_offset;
+  // Is this field optional?
+  bool optional;
+  // The name of the field.
+  const char* name;
+  // The key to use with JsonArgs to see if this field is enabled.
+  const char* enable_key;
+};
+
+// Vec<T, kSize> provides a constant array type that can be appended to by
+// copying. It's setup so that most compilers can optimize away all of its
+// operations.
+template <typename T, size_t kSize>
+class Vec {
+ public:
+  Vec(const Vec<T, kSize - 1>& other, const T& new_value) {
+    for (size_t i = 0; i < other.size(); i++) values_[i] = other.data()[i];
+    values_[kSize - 1] = new_value;
+  }
+
+  const T* data() const { return values_; }
+  size_t size() const { return kSize; }
+
+ private:
+  T values_[kSize];
+};
+
+template <typename T>
+class Vec<T, 0> {
+ public:
+  const T* data() const { return nullptr; }
+  size_t size() const { return 0; }
+};
+
+// Given a list of elements, and a destination object, load the elements into
+// the object from some parsed JSON.
+// Returns false if the JSON object was not of type Json::Type::OBJECT.
+bool LoadObject(const Json& json, const JsonArgs& args, const Element* elements,
+                size_t num_elements, void* dst, ErrorList* errors);
+
+// Adaptor type - takes a compile time computed list of elements and implements
+// LoaderInterface by calling LoadObject.
+template <typename T, size_t kElemCount, typename Hidden = void>
+class FinishedJsonObjectLoader final : public LoaderInterface {
+ public:
+  explicit FinishedJsonObjectLoader(const Vec<Element, kElemCount>& elements)
+      : elements_(elements) {}
+
+  void LoadInto(const Json& json, const JsonArgs& args, void* dst,
+                ErrorList* errors) const override {
+    LoadObject(json, args, elements_.data(), elements_.size(), dst, errors);
+  }
+
+ private:
+  GPR_NO_UNIQUE_ADDRESS Vec<Element, kElemCount> elements_;
+};
+
+// Specialization for when the object has a JsonPostLoad function exposed.
+template <typename T, size_t kElemCount>
+class FinishedJsonObjectLoader<T, kElemCount,
+                               absl::void_t<decltype(&T::JsonPostLoad)>>
+    final : public LoaderInterface {
+ public:
+  explicit FinishedJsonObjectLoader(const Vec<Element, kElemCount>& elements)
+      : elements_(elements) {}
+
+  void LoadInto(const Json& json, const JsonArgs& args, void* dst,
+                ErrorList* errors) const override {
+    // Call JsonPostLoad() only if json is a JSON object.
+    if (LoadObject(json, args, elements_.data(), elements_.size(), dst,
+                   errors)) {
+      static_cast<T*>(dst)->JsonPostLoad(json, args, errors);
+    }
+  }
+
+ private:
+  GPR_NO_UNIQUE_ADDRESS Vec<Element, kElemCount> elements_;
+};
+
+// Builder type for JSON object loaders.
+// Concatenate fields with Field, OptionalField, and then call Finish to obtain
+// an object that implements LoaderInterface.
+template <typename T, size_t kElemCount = 0>
+class JsonObjectLoader final {
+ public:
+  JsonObjectLoader() {
+    static_assert(kElemCount == 0,
+                  "Only initial loader step can have kElemCount==0.");
+  }
+
+  FinishedJsonObjectLoader<T, kElemCount>* Finish() const {
+    return new FinishedJsonObjectLoader<T, kElemCount>(elements_);
+  }
+
+  template <typename U>
+  JsonObjectLoader<T, kElemCount + 1> Field(
+      const char* name, U T::*p, const char* enable_key = nullptr) const {
+    return Field(name, false, p, enable_key);
+  }
+
+  template <typename U>
+  JsonObjectLoader<T, kElemCount + 1> OptionalField(
+      const char* name, U T::*p, const char* enable_key = nullptr) const {
+    return Field(name, true, p, enable_key);
+  }
+
+  JsonObjectLoader(const Vec<Element, kElemCount - 1>& elements,
+                   Element new_element)
+      : elements_(elements, new_element) {}
+
+ private:
+  template <typename U>
+  JsonObjectLoader<T, kElemCount + 1> Field(const char* name, bool optional,
+                                            U T::*p,
+                                            const char* enable_key) const {
+    return JsonObjectLoader<T, kElemCount + 1>(
+        elements_, Element(name, optional, p, LoaderForType<U>(), enable_key));
+  }
+
+  GPR_NO_UNIQUE_ADDRESS Vec<Element, kElemCount> elements_;
+};
+
+const Json* GetJsonObjectField(const Json::Object& json,
+                               absl::string_view field, ErrorList* errors,
+                               bool required);
+
+}  // namespace json_detail
+
+template <typename T>
+using JsonObjectLoader = json_detail::JsonObjectLoader<T>;
+
+using JsonLoaderInterface = json_detail::LoaderInterface;
+
+template <typename T>
+absl::StatusOr<T> LoadFromJson(const Json& json,
+                               const JsonArgs& args = JsonArgs()) {
+  ErrorList error_list;
+  T result{};
+  json_detail::LoaderForType<T>()->LoadInto(json, args, &result, &error_list);
+  if (!error_list.ok()) return error_list.status();
+  return std::move(result);
+}
+
+template <typename T>
+T LoadFromJson(const Json& json, const JsonArgs& args, ErrorList* error_list) {
+  T result{};
+  json_detail::LoaderForType<T>()->LoadInto(json, args, &result, error_list);
+  return result;
+}
+
+template <typename T>
+absl::optional<T> LoadJsonObjectField(const Json::Object& json,
+                                      const JsonArgs& args,
+                                      absl::string_view field,
+                                      ErrorList* errors, bool required = true) {
+  ScopedField error_field(errors, absl::StrCat(".", field));
+  const Json* field_json =
+      json_detail::GetJsonObjectField(json, field, errors, required);
+  if (field_json == nullptr) return absl::nullopt;
+  T result{};
+  size_t starting_error_size = errors->size();
+  json_detail::LoaderForType<T>()->LoadInto(*field_json, args, &result, errors);
+  if (errors->size() > starting_error_size) return absl::nullopt;
+  return std::move(result);
+}
+
+}  // namespace grpc_core
+
+#endif  // GRPC_CORE_LIB_JSON_JSON_OBJECT_LOADER_H
diff --git a/src/core/lib/json/json_util.cc b/src/core/lib/json/json_util.cc
index 0d1b5ce..451a67f 100644
--- a/src/core/lib/json/json_util.cc
+++ b/src/core/lib/json/json_util.cc
@@ -20,46 +20,17 @@
 
 #include "src/core/lib/json/json_util.h"
 
-#include <string.h>
-
-#include <grpc/support/string_util.h>
-
-#include "src/core/lib/gpr/string.h"
-#include "src/core/lib/gprpp/memory.h"
+#include "src/core/lib/json/json_args.h"
+#include "src/core/lib/json/json_object_loader.h"
 
 namespace grpc_core {
 
 bool ParseDurationFromJson(const Json& field, Duration* duration) {
-  if (field.type() != Json::Type::STRING) return false;
-  size_t len = field.string_value().size();
-  if (field.string_value()[len - 1] != 's') return false;
-  if (field.string_value() == Duration::Infinity().ToJsonString()) {
-    *duration = Duration::Infinity();
-    return true;
-  }
-  UniquePtr<char> buf(gpr_strdup(field.string_value().c_str()));
-  *(buf.get() + len - 1) = '\0';  // Remove trailing 's'.
-  char* decimal_point = strchr(buf.get(), '.');
-  int nanos = 0;
-  if (decimal_point != nullptr) {
-    *decimal_point = '\0';
-    nanos = gpr_parse_nonnegative_int(decimal_point + 1);
-    if (nanos == -1) {
-      return false;
-    }
-    int num_digits = static_cast<int>(strlen(decimal_point + 1));
-    if (num_digits > 9) {  // We don't accept greater precision than nanos.
-      return false;
-    }
-    for (int i = 0; i < (9 - num_digits); ++i) {
-      nanos *= 10;
-    }
-  }
-  int seconds =
-      decimal_point == buf.get() ? 0 : gpr_parse_nonnegative_int(buf.get());
-  if (seconds == -1) return false;
-  *duration = Duration::FromSecondsAndNanoseconds(seconds, nanos);
-  return true;
+  json_detail::AutoLoader<Duration> loader;
+  ErrorList errors;
+  static_cast<json_detail::LoaderInterface&>(loader).LoadInto(
+      field, JsonArgs(), duration, &errors);
+  return errors.ok();
 }
 
 bool ExtractJsonBool(const Json& json, absl::string_view field_name,
diff --git a/src/python/grpcio/grpc_core_dependencies.py b/src/python/grpcio/grpc_core_dependencies.py
index b4a4373..854bdb9 100644
--- a/src/python/grpcio/grpc_core_dependencies.py
+++ b/src/python/grpcio/grpc_core_dependencies.py
@@ -582,6 +582,7 @@
     'src/core/lib/iomgr/wakeup_fd_nospecial.cc',
     'src/core/lib/iomgr/wakeup_fd_pipe.cc',
     'src/core/lib/iomgr/wakeup_fd_posix.cc',
+    'src/core/lib/json/json_object_loader.cc',
     'src/core/lib/json/json_reader.cc',
     'src/core/lib/json/json_util.cc',
     'src/core/lib/json/json_writer.cc',
diff --git a/test/core/json/BUILD b/test/core/json/BUILD
index 7f704c3..7eafc56 100644
--- a/test/core/json/BUILD
+++ b/test/core/json/BUILD
@@ -47,3 +47,17 @@
         "//test/core/util:grpc_test_util",
     ],
 )
+
+grpc_cc_test(
+    name = "json_object_loader_test",
+    srcs = ["json_object_loader_test.cc"],
+    external_deps = [
+        "gtest",
+    ],
+    language = "C++",
+    uses_polling = False,
+    deps = [
+        "//:json_object_loader",
+        "//test/core/util:grpc_test_util",
+    ],
+)
diff --git a/test/core/json/json_object_loader_test.cc b/test/core/json/json_object_loader_test.cc
new file mode 100644
index 0000000..10b8fb5
--- /dev/null
+++ b/test/core/json/json_object_loader_test.cc
@@ -0,0 +1,984 @@
+// Copyright 2021 gRPC authors.
+//
+// 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 "src/core/lib/json/json_object_loader.h"
+
+#include <cstdint>
+
+#include <gmock/gmock.h>
+#include <gtest/gtest.h>
+
+#include "absl/strings/str_join.h"
+
+namespace grpc_core {
+namespace {
+
+template <typename T>
+absl::StatusOr<T> Parse(absl::string_view json,
+                        const JsonArgs& args = JsonArgs()) {
+  auto parsed = Json::Parse(json);
+  if (!parsed.ok()) return parsed.status();
+  return LoadFromJson<T>(*parsed, args);
+}
+
+//
+// Signed integer tests
+//
+
+template <typename T>
+class SignedIntegerTest : public ::testing::Test {};
+
+TYPED_TEST_SUITE_P(SignedIntegerTest);
+
+TYPED_TEST_P(SignedIntegerTest, IntegerFields) {
+  struct TestStruct {
+    TypeParam value = 0;
+    TypeParam optional_value = 0;
+    absl::optional<TypeParam> absl_optional_value;
+
+    static const JsonLoaderInterface* JsonLoader(const JsonArgs&) {
+      static const auto* loader =
+          JsonObjectLoader<TestStruct>()
+              .Field("value", &TestStruct::value)
+              .OptionalField("optional_value", &TestStruct::optional_value)
+              .OptionalField("absl_optional_value",
+                             &TestStruct::absl_optional_value)
+              .Finish();
+      return loader;
+    }
+  };
+  // Positive number.
+  auto test_struct = Parse<TestStruct>("{\"value\": 5}");
+  ASSERT_TRUE(test_struct.ok()) << test_struct.status();
+  EXPECT_EQ(test_struct->value, 5);
+  EXPECT_EQ(test_struct->optional_value, 0);
+  EXPECT_FALSE(test_struct->absl_optional_value.has_value());
+  // Negative number.
+  test_struct = Parse<TestStruct>("{\"value\": -5}");
+  ASSERT_TRUE(test_struct.ok()) << test_struct.status();
+  EXPECT_EQ(test_struct->value, -5);
+  EXPECT_EQ(test_struct->optional_value, 0);
+  EXPECT_FALSE(test_struct->absl_optional_value.has_value());
+  // Encoded in a JSON string.
+  test_struct = Parse<TestStruct>("{\"value\": \"5\"}");
+  ASSERT_TRUE(test_struct.ok()) << test_struct.status();
+  EXPECT_EQ(test_struct->value, 5);
+  EXPECT_EQ(test_struct->optional_value, 0);
+  EXPECT_FALSE(test_struct->absl_optional_value.has_value());
+  // Fails if required field is not present.
+  test_struct = Parse<TestStruct>("{}");
+  EXPECT_EQ(test_struct.status().code(), absl::StatusCode::kInvalidArgument);
+  EXPECT_EQ(test_struct.status().message(),
+            "errors validating JSON: [field:value error:field not present]")
+      << test_struct.status();
+  // Optional fields present.
+  test_struct = Parse<TestStruct>(
+      "{\"value\": 5, \"optional_value\": 7, "
+      "\"absl_optional_value\": 9}");
+  ASSERT_TRUE(test_struct.ok()) << test_struct.status();
+  EXPECT_EQ(test_struct->value, 5);
+  EXPECT_EQ(test_struct->optional_value, 7);
+  EXPECT_EQ(test_struct->absl_optional_value, 9);
+  // Wrong JSON type.
+  test_struct = Parse<TestStruct>(
+      "{\"value\": [], \"optional_value\": {}, "
+      "\"absl_optional_value\": true}");
+  EXPECT_EQ(test_struct.status().code(), absl::StatusCode::kInvalidArgument);
+  EXPECT_EQ(test_struct.status().message(),
+            "errors validating JSON: ["
+            "field:absl_optional_value error:is not a number; "
+            "field:optional_value error:is not a number; "
+            "field:value error:is not a number]")
+      << test_struct.status();
+}
+
+REGISTER_TYPED_TEST_SUITE_P(SignedIntegerTest, IntegerFields);
+
+using IntegerTypes = ::testing::Types<int32_t, int64_t>;
+INSTANTIATE_TYPED_TEST_SUITE_P(My, SignedIntegerTest, IntegerTypes);
+
+//
+// Unsigned integer tests
+//
+
+template <typename T>
+class UnsignedIntegerTest : public ::testing::Test {};
+
+TYPED_TEST_SUITE_P(UnsignedIntegerTest);
+
+TYPED_TEST_P(UnsignedIntegerTest, IntegerFields) {
+  struct TestStruct {
+    TypeParam value = 0;
+    TypeParam optional_value = 0;
+    absl::optional<TypeParam> absl_optional_value;
+
+    static const JsonLoaderInterface* JsonLoader(const JsonArgs&) {
+      static const auto* loader =
+          JsonObjectLoader<TestStruct>()
+              .Field("value", &TestStruct::value)
+              .OptionalField("optional_value", &TestStruct::optional_value)
+              .OptionalField("absl_optional_value",
+                             &TestStruct::absl_optional_value)
+              .Finish();
+      return loader;
+    }
+  };
+  // Positive number.
+  auto test_struct = Parse<TestStruct>("{\"value\": 5}");
+  ASSERT_TRUE(test_struct.ok()) << test_struct.status();
+  EXPECT_EQ(test_struct->value, 5);
+  EXPECT_EQ(test_struct->optional_value, 0);
+  EXPECT_FALSE(test_struct->absl_optional_value.has_value());
+  // Negative number.
+  test_struct = Parse<TestStruct>("{\"value\": -5}");
+  EXPECT_EQ(test_struct.status().code(), absl::StatusCode::kInvalidArgument);
+  EXPECT_EQ(test_struct.status().message(),
+            "errors validating JSON: ["
+            "field:value error:failed to parse non-negative number]")
+      << test_struct.status();
+  // Encoded in a JSON string.
+  test_struct = Parse<TestStruct>("{\"value\": \"5\"}");
+  ASSERT_TRUE(test_struct.ok()) << test_struct.status();
+  EXPECT_EQ(test_struct->value, 5);
+  EXPECT_EQ(test_struct->optional_value, 0);
+  EXPECT_FALSE(test_struct->absl_optional_value.has_value());
+  // Fails if required field is not present.
+  test_struct = Parse<TestStruct>("{}");
+  EXPECT_EQ(test_struct.status().code(), absl::StatusCode::kInvalidArgument);
+  EXPECT_EQ(test_struct.status().message(),
+            "errors validating JSON: [field:value error:field not present]")
+      << test_struct.status();
+  // Optional fields present.
+  test_struct = Parse<TestStruct>(
+      "{\"value\": 5, \"optional_value\": 7, "
+      "\"absl_optional_value\": 9}");
+  ASSERT_TRUE(test_struct.ok()) << test_struct.status();
+  EXPECT_EQ(test_struct->value, 5);
+  EXPECT_EQ(test_struct->optional_value, 7);
+  ASSERT_TRUE(test_struct->absl_optional_value.has_value());
+  EXPECT_EQ(*test_struct->absl_optional_value, 9);
+  // Wrong JSON type.
+  test_struct = Parse<TestStruct>(
+      "{\"value\": [], \"optional_value\": {}, "
+      "\"absl_optional_value\": true}");
+  EXPECT_EQ(test_struct.status().code(), absl::StatusCode::kInvalidArgument);
+  EXPECT_EQ(test_struct.status().message(),
+            "errors validating JSON: ["
+            "field:absl_optional_value error:is not a number; "
+            "field:optional_value error:is not a number; "
+            "field:value error:is not a number]")
+      << test_struct.status();
+}
+
+REGISTER_TYPED_TEST_SUITE_P(UnsignedIntegerTest, IntegerFields);
+
+using UnsignedIntegerTypes = ::testing::Types<uint32_t, uint64_t>;
+INSTANTIATE_TYPED_TEST_SUITE_P(My, UnsignedIntegerTest, UnsignedIntegerTypes);
+
+//
+// Floating-point tests
+//
+
+template <typename T>
+class FloatingPointTest : public ::testing::Test {};
+
+TYPED_TEST_SUITE_P(FloatingPointTest);
+
+TYPED_TEST_P(FloatingPointTest, FloatFields) {
+  struct TestStruct {
+    TypeParam value = 0;
+    TypeParam optional_value = 0;
+    absl::optional<TypeParam> absl_optional_value;
+
+    static const JsonLoaderInterface* JsonLoader(const JsonArgs&) {
+      static const auto* loader =
+          JsonObjectLoader<TestStruct>()
+              .Field("value", &TestStruct::value)
+              .OptionalField("optional_value", &TestStruct::optional_value)
+              .OptionalField("absl_optional_value",
+                             &TestStruct::absl_optional_value)
+              .Finish();
+      return loader;
+    }
+  };
+  // Positive number.
+  auto test_struct = Parse<TestStruct>("{\"value\": 5.2}");
+  ASSERT_TRUE(test_struct.ok()) << test_struct.status();
+  EXPECT_NEAR(test_struct->value, 5.2, 0.0001);
+  EXPECT_EQ(test_struct->optional_value, 0);
+  EXPECT_FALSE(test_struct->absl_optional_value.has_value());
+  // Negative number.
+  test_struct = Parse<TestStruct>("{\"value\": -5.2}");
+  ASSERT_TRUE(test_struct.ok()) << test_struct.status();
+  EXPECT_NEAR(test_struct->value, -5.2, 0.0001);
+  EXPECT_EQ(test_struct->optional_value, 0);
+  EXPECT_FALSE(test_struct->absl_optional_value.has_value());
+  // Encoded in a JSON string.
+  test_struct = Parse<TestStruct>("{\"value\": \"5.2\"}");
+  ASSERT_TRUE(test_struct.ok()) << test_struct.status();
+  EXPECT_NEAR(test_struct->value, 5.2, 0.0001);
+  EXPECT_EQ(test_struct->optional_value, 0);
+  EXPECT_FALSE(test_struct->absl_optional_value.has_value());
+  // Fails if required field is not present.
+  test_struct = Parse<TestStruct>("{}");
+  EXPECT_EQ(test_struct.status().code(), absl::StatusCode::kInvalidArgument);
+  EXPECT_EQ(test_struct.status().message(),
+            "errors validating JSON: [field:value error:field not present]")
+      << test_struct.status();
+  // Optional fields present.
+  test_struct = Parse<TestStruct>(
+      "{\"value\": 5.2, \"optional_value\": 7.5, "
+      "\"absl_optional_value\": 9.8}");
+  ASSERT_TRUE(test_struct.ok()) << test_struct.status();
+  EXPECT_NEAR(test_struct->value, 5.2, 0.0001);
+  EXPECT_NEAR(test_struct->optional_value, 7.5, 0.0001);
+  ASSERT_TRUE(test_struct->absl_optional_value.has_value());
+  EXPECT_NEAR(*test_struct->absl_optional_value, 9.8, 0.0001);
+  // Wrong JSON type.
+  test_struct = Parse<TestStruct>(
+      "{\"value\": [], \"optional_value\": {}, "
+      "\"absl_optional_value\": true}");
+  EXPECT_EQ(test_struct.status().code(), absl::StatusCode::kInvalidArgument);
+  EXPECT_EQ(test_struct.status().message(),
+            "errors validating JSON: ["
+            "field:absl_optional_value error:is not a number; "
+            "field:optional_value error:is not a number; "
+            "field:value error:is not a number]")
+      << test_struct.status();
+}
+
+REGISTER_TYPED_TEST_SUITE_P(FloatingPointTest, FloatFields);
+
+using FloatingPointTypes = ::testing::Types<float, double>;
+INSTANTIATE_TYPED_TEST_SUITE_P(My, FloatingPointTest, FloatingPointTypes);
+
+//
+// Boolean tests
+//
+
+TEST(JsonObjectLoader, BooleanFields) {
+  struct TestStruct {
+    bool value = false;
+    bool optional_value = true;
+    absl::optional<bool> absl_optional_value;
+
+    static const JsonLoaderInterface* JsonLoader(const JsonArgs&) {
+      static const auto* loader =
+          JsonObjectLoader<TestStruct>()
+              .Field("value", &TestStruct::value)
+              .OptionalField("optional_value", &TestStruct::optional_value)
+              .OptionalField("absl_optional_value",
+                             &TestStruct::absl_optional_value)
+              .Finish();
+      return loader;
+    }
+  };
+  // True.
+  auto test_struct = Parse<TestStruct>("{\"value\": true}");
+  ASSERT_TRUE(test_struct.ok()) << test_struct.status();
+  EXPECT_EQ(test_struct->value, true);
+  EXPECT_EQ(test_struct->optional_value, true);  // Unmodified.
+  EXPECT_FALSE(test_struct->absl_optional_value.has_value());
+  // False.
+  test_struct = Parse<TestStruct>("{\"value\": false}");
+  ASSERT_TRUE(test_struct.ok()) << test_struct.status();
+  EXPECT_EQ(test_struct->value, false);
+  EXPECT_EQ(test_struct->optional_value, true);  // Unmodified.
+  EXPECT_FALSE(test_struct->absl_optional_value.has_value());
+  // Fails if required field is not present.
+  test_struct = Parse<TestStruct>("{}");
+  EXPECT_EQ(test_struct.status().code(), absl::StatusCode::kInvalidArgument);
+  EXPECT_EQ(test_struct.status().message(),
+            "errors validating JSON: [field:value error:field not present]")
+      << test_struct.status();
+  // Optional fields present.
+  test_struct = Parse<TestStruct>(
+      "{\"value\": true, \"optional_value\": false,"
+      "\"absl_optional_value\": true}");
+  ASSERT_TRUE(test_struct.ok()) << test_struct.status();
+  EXPECT_EQ(test_struct->value, true);
+  EXPECT_EQ(test_struct->optional_value, false);
+  EXPECT_EQ(test_struct->absl_optional_value, true);
+  // Wrong JSON type.
+  test_struct = Parse<TestStruct>(
+      "{\"value\": [], \"optional_value\": {}, "
+      "\"absl_optional_value\": 1}");
+  EXPECT_EQ(test_struct.status().code(), absl::StatusCode::kInvalidArgument);
+  EXPECT_EQ(test_struct.status().message(),
+            "errors validating JSON: ["
+            "field:absl_optional_value error:is not a boolean; "
+            "field:optional_value error:is not a boolean; "
+            "field:value error:is not a boolean]")
+      << test_struct.status();
+}
+
+//
+// String tests
+//
+
+TEST(JsonObjectLoader, StringFields) {
+  struct TestStruct {
+    std::string value;
+    std::string optional_value;
+    absl::optional<std::string> absl_optional_value;
+
+    static const JsonLoaderInterface* JsonLoader(const JsonArgs&) {
+      static const auto* loader =
+          JsonObjectLoader<TestStruct>()
+              .Field("value", &TestStruct::value)
+              .OptionalField("optional_value", &TestStruct::optional_value)
+              .OptionalField("absl_optional_value",
+                             &TestStruct::absl_optional_value)
+              .Finish();
+      return loader;
+    }
+  };
+  // Valid string.
+  auto test_struct = Parse<TestStruct>("{\"value\": \"foo\"}");
+  ASSERT_TRUE(test_struct.ok()) << test_struct.status();
+  EXPECT_EQ(test_struct->value, "foo");
+  EXPECT_EQ(test_struct->optional_value, "");
+  EXPECT_FALSE(test_struct->absl_optional_value.has_value());
+  // Fails if required field is not present.
+  test_struct = Parse<TestStruct>("{}");
+  EXPECT_EQ(test_struct.status().code(), absl::StatusCode::kInvalidArgument);
+  EXPECT_EQ(test_struct.status().message(),
+            "errors validating JSON: [field:value error:field not present]")
+      << test_struct.status();
+  // Optional fields present.
+  test_struct = Parse<TestStruct>(
+      "{\"value\": \"foo\", \"optional_value\": \"bar\","
+      "\"absl_optional_value\": \"baz\"}");
+  ASSERT_TRUE(test_struct.ok()) << test_struct.status();
+  EXPECT_EQ(test_struct->value, "foo");
+  EXPECT_EQ(test_struct->optional_value, "bar");
+  EXPECT_EQ(test_struct->absl_optional_value, "baz");
+  // Wrong JSON type.
+  test_struct = Parse<TestStruct>(
+      "{\"value\": [], \"optional_value\": {}, "
+      "\"absl_optional_value\": 1}");
+  EXPECT_EQ(test_struct.status().code(), absl::StatusCode::kInvalidArgument);
+  EXPECT_EQ(test_struct.status().message(),
+            "errors validating JSON: ["
+            "field:absl_optional_value error:is not a string; "
+            "field:optional_value error:is not a string; "
+            "field:value error:is not a string]")
+      << test_struct.status();
+}
+
+//
+// Duration tests
+//
+
+TEST(JsonObjectLoader, DurationFields) {
+  struct TestStruct {
+    Duration value = Duration::Zero();
+    Duration optional_value = Duration::Zero();
+    absl::optional<Duration> absl_optional_value;
+
+    static const JsonLoaderInterface* JsonLoader(const JsonArgs&) {
+      static const auto* loader =
+          JsonObjectLoader<TestStruct>()
+              .Field("value", &TestStruct::value)
+              .OptionalField("optional_value", &TestStruct::optional_value)
+              .OptionalField("absl_optional_value",
+                             &TestStruct::absl_optional_value)
+              .Finish();
+      return loader;
+    }
+  };
+  // Valid duration string.
+  auto test_struct = Parse<TestStruct>("{\"value\": \"3s\"}");
+  ASSERT_TRUE(test_struct.ok()) << test_struct.status();
+  EXPECT_EQ(test_struct->value, Duration::Seconds(3));
+  EXPECT_EQ(test_struct->optional_value, Duration::Zero());
+  EXPECT_FALSE(test_struct->absl_optional_value.has_value());
+  // Invalid duration strings.
+  test_struct = Parse<TestStruct>(
+      "{\"value\": \"3sec\", \"optional_value\": \"foos\","
+      "\"absl_optional_value\": \"1.0123456789s\"}");
+  EXPECT_EQ(test_struct.status().code(), absl::StatusCode::kInvalidArgument);
+  EXPECT_EQ(test_struct.status().message(),
+            "errors validating JSON: ["
+            "field:absl_optional_value error:"
+            "Not a duration (too many digits after decimal); "
+            "field:optional_value error:"
+            "Not a duration (not a number of seconds); "
+            "field:value error:Not a duration (no s suffix)]")
+      << test_struct.status();
+  test_struct = Parse<TestStruct>("{\"value\": \"3.xs\"}");
+  EXPECT_EQ(test_struct.status().code(), absl::StatusCode::kInvalidArgument);
+  EXPECT_EQ(test_struct.status().message(),
+            "errors validating JSON: ["
+            "field:value error:Not a duration (not a number of nanoseconds)]")
+      << test_struct.status();
+  // Fails if required field is not present.
+  test_struct = Parse<TestStruct>("{}");
+  EXPECT_EQ(test_struct.status().code(), absl::StatusCode::kInvalidArgument);
+  EXPECT_EQ(test_struct.status().message(),
+            "errors validating JSON: [field:value error:field not present]")
+      << test_struct.status();
+  // Optional fields present.
+  test_struct = Parse<TestStruct>(
+      "{\"value\": \"3s\", \"optional_value\": \"3.2s\", "
+      "\"absl_optional_value\": \"10s\"}");
+  ASSERT_TRUE(test_struct.ok()) << test_struct.status();
+  EXPECT_EQ(test_struct->value, Duration::Seconds(3));
+  EXPECT_EQ(test_struct->optional_value, Duration::Milliseconds(3200));
+  EXPECT_EQ(test_struct->absl_optional_value, Duration::Seconds(10));
+  // Wrong JSON type.
+  test_struct = Parse<TestStruct>(
+      "{\"value\": [], \"optional_value\": {}, "
+      "\"absl_optional_value\": 1}");
+  EXPECT_EQ(test_struct.status().code(), absl::StatusCode::kInvalidArgument);
+  EXPECT_EQ(test_struct.status().message(),
+            "errors validating JSON: ["
+            "field:absl_optional_value error:is not a string; "
+            "field:optional_value error:is not a string; "
+            "field:value error:is not a string]")
+      << test_struct.status();
+}
+
+//
+// Json::Object tests
+//
+
+TEST(JsonObjectLoader, JsonObjectFields) {
+  struct TestStruct {
+    Json::Object value;
+    Json::Object optional_value;
+    absl::optional<Json::Object> absl_optional_value;
+
+    static const JsonLoaderInterface* JsonLoader(const JsonArgs&) {
+      static const auto* loader =
+          JsonObjectLoader<TestStruct>()
+              .Field("value", &TestStruct::value)
+              .OptionalField("optional_value", &TestStruct::optional_value)
+              .OptionalField("absl_optional_value",
+                             &TestStruct::absl_optional_value)
+              .Finish();
+      return loader;
+    }
+  };
+  // Valid object.
+  auto test_struct = Parse<TestStruct>("{\"value\": {\"a\":1}}");
+  ASSERT_TRUE(test_struct.ok()) << test_struct.status();
+  EXPECT_EQ(Json{test_struct->value}.Dump(), "{\"a\":1}");
+  EXPECT_EQ(Json{test_struct->optional_value}.Dump(), "{}");
+  EXPECT_FALSE(test_struct->absl_optional_value.has_value());
+  // Fails if required field is not present.
+  test_struct = Parse<TestStruct>("{}");
+  EXPECT_EQ(test_struct.status().code(), absl::StatusCode::kInvalidArgument);
+  EXPECT_EQ(test_struct.status().message(),
+            "errors validating JSON: [field:value error:field not present]")
+      << test_struct.status();
+  // Optional fields present.
+  test_struct = Parse<TestStruct>(
+      "{\"value\": {\"a\":1}, \"optional_value\": {\"b\":2}, "
+      "\"absl_optional_value\": {\"c\":3}}");
+  ASSERT_TRUE(test_struct.ok()) << test_struct.status();
+  EXPECT_EQ(Json{test_struct->value}.Dump(), "{\"a\":1}");
+  EXPECT_EQ(Json{test_struct->optional_value}.Dump(), "{\"b\":2}");
+  ASSERT_TRUE(test_struct->absl_optional_value.has_value());
+  EXPECT_EQ(Json{*test_struct->absl_optional_value}.Dump(), "{\"c\":3}");
+  // Wrong JSON type.
+  test_struct = Parse<TestStruct>(
+      "{\"value\": [], \"optional_value\": true, "
+      "\"absl_optional_value\": 1}");
+  EXPECT_EQ(test_struct.status().code(), absl::StatusCode::kInvalidArgument);
+  EXPECT_EQ(test_struct.status().message(),
+            "errors validating JSON: ["
+            "field:absl_optional_value error:is not an object; "
+            "field:optional_value error:is not an object; "
+            "field:value error:is not an object]")
+      << test_struct.status();
+}
+
+//
+// map<> tests
+//
+
+TEST(JsonObjectLoader, MapFields) {
+  struct TestStruct {
+    std::map<std::string, int32_t> value;
+    std::map<std::string, std::string> optional_value;
+    absl::optional<std::map<std::string, bool>> absl_optional_value;
+
+    static const JsonLoaderInterface* JsonLoader(const JsonArgs&) {
+      static const auto* loader =
+          JsonObjectLoader<TestStruct>()
+              .Field("value", &TestStruct::value)
+              .OptionalField("optional_value", &TestStruct::optional_value)
+              .OptionalField("absl_optional_value",
+                             &TestStruct::absl_optional_value)
+              .Finish();
+      return loader;
+    }
+  };
+  // Valid map.
+  auto test_struct = Parse<TestStruct>("{\"value\": {\"a\":1}}");
+  ASSERT_TRUE(test_struct.ok()) << test_struct.status();
+  EXPECT_THAT(test_struct->value,
+              ::testing::ElementsAre(::testing::Pair("a", 1)));
+  EXPECT_THAT(test_struct->optional_value, ::testing::ElementsAre());
+  EXPECT_FALSE(test_struct->absl_optional_value.has_value());
+  // Fails if required field is not present.
+  test_struct = Parse<TestStruct>("{}");
+  EXPECT_EQ(test_struct.status().code(), absl::StatusCode::kInvalidArgument);
+  EXPECT_EQ(test_struct.status().message(),
+            "errors validating JSON: [field:value error:field not present]")
+      << test_struct.status();
+  // Optional fields present.
+  test_struct = Parse<TestStruct>(
+      "{\"value\": {\"a\":1}, \"optional_value\": {\"b\":\"foo\"}, "
+      "\"absl_optional_value\": {\"c\":true}}");
+  ASSERT_TRUE(test_struct.ok()) << test_struct.status();
+  EXPECT_THAT(test_struct->value,
+              ::testing::ElementsAre(::testing::Pair("a", 1)));
+  EXPECT_THAT(test_struct->optional_value,
+              ::testing::ElementsAre(::testing::Pair("b", "foo")));
+  ASSERT_TRUE(test_struct->absl_optional_value.has_value());
+  EXPECT_THAT(*test_struct->absl_optional_value,
+              ::testing::ElementsAre(::testing::Pair("c", true)));
+  // Wrong JSON type.
+  test_struct = Parse<TestStruct>(
+      "{\"value\": [], \"optional_value\": true, "
+      "\"absl_optional_value\": 1}");
+  EXPECT_EQ(test_struct.status().code(), absl::StatusCode::kInvalidArgument);
+  EXPECT_EQ(test_struct.status().message(),
+            "errors validating JSON: ["
+            "field:absl_optional_value error:is not an object; "
+            "field:optional_value error:is not an object; "
+            "field:value error:is not an object]")
+      << test_struct.status();
+  // Wrong JSON type for map value.
+  test_struct = Parse<TestStruct>(
+      "{\"value\": {\"a\":\"foo\"}, \"optional_value\": {\"b\":true}, "
+      "\"absl_optional_value\": {\"c\":1}}");
+  EXPECT_EQ(test_struct.status().code(), absl::StatusCode::kInvalidArgument);
+  EXPECT_EQ(test_struct.status().message(),
+            "errors validating JSON: ["
+            "field:absl_optional_value[\"c\"] error:is not a boolean; "
+            "field:optional_value[\"b\"] error:is not a string; "
+            "field:value[\"a\"] error:failed to parse number]")
+      << test_struct.status();
+}
+
+//
+// vector<> tests
+//
+
+TEST(JsonObjectLoader, VectorFields) {
+  struct TestStruct {
+    std::vector<int32_t> value;
+    std::vector<std::string> optional_value;
+    absl::optional<std::vector<bool>> absl_optional_value;
+
+    static const JsonLoaderInterface* JsonLoader(const JsonArgs&) {
+      static const auto* loader =
+          JsonObjectLoader<TestStruct>()
+              .Field("value", &TestStruct::value)
+              .OptionalField("optional_value", &TestStruct::optional_value)
+              .OptionalField("absl_optional_value",
+                             &TestStruct::absl_optional_value)
+              .Finish();
+      return loader;
+    }
+  };
+  // Valid map.
+  auto test_struct = Parse<TestStruct>("{\"value\": [1, 2, 3]}");
+  ASSERT_TRUE(test_struct.ok()) << test_struct.status();
+  EXPECT_THAT(test_struct->value, ::testing::ElementsAre(1, 2, 3));
+  EXPECT_THAT(test_struct->optional_value, ::testing::ElementsAre());
+  EXPECT_FALSE(test_struct->absl_optional_value.has_value());
+  // Fails if required field is not present.
+  test_struct = Parse<TestStruct>("{}");
+  EXPECT_EQ(test_struct.status().code(), absl::StatusCode::kInvalidArgument);
+  EXPECT_EQ(test_struct.status().message(),
+            "errors validating JSON: [field:value error:field not present]")
+      << test_struct.status();
+  // Optional fields present.
+  test_struct = Parse<TestStruct>(
+      "{\"value\": [4, 5, 6], \"optional_value\": [\"foo\", \"bar\"], "
+      "\"absl_optional_value\": [true, false, true]}");
+  ASSERT_TRUE(test_struct.ok()) << test_struct.status();
+  EXPECT_THAT(test_struct->value, ::testing::ElementsAre(4, 5, 6));
+  EXPECT_THAT(test_struct->optional_value,
+              ::testing::ElementsAre("foo", "bar"));
+  ASSERT_TRUE(test_struct->absl_optional_value.has_value());
+  EXPECT_THAT(*test_struct->absl_optional_value,
+              ::testing::ElementsAre(true, false, true));
+  // Wrong JSON type.
+  test_struct = Parse<TestStruct>(
+      "{\"value\": {}, \"optional_value\": true, "
+      "\"absl_optional_value\": 1}");
+  EXPECT_EQ(test_struct.status().code(), absl::StatusCode::kInvalidArgument);
+  EXPECT_EQ(test_struct.status().message(),
+            "errors validating JSON: ["
+            "field:absl_optional_value error:is not an array; "
+            "field:optional_value error:is not an array; "
+            "field:value error:is not an array]")
+      << test_struct.status();
+  // Wrong JSON type for map value.
+  test_struct = Parse<TestStruct>(
+      "{\"value\": [\"foo\", \"bar\"], \"optional_value\": [true, false], "
+      "\"absl_optional_value\": [1, 2]}");
+  EXPECT_EQ(test_struct.status().code(), absl::StatusCode::kInvalidArgument);
+  EXPECT_EQ(test_struct.status().message(),
+            "errors validating JSON: ["
+            "field:absl_optional_value[0] error:is not a boolean; "
+            "field:absl_optional_value[1] error:is not a boolean; "
+            "field:optional_value[0] error:is not a string; "
+            "field:optional_value[1] error:is not a string; "
+            "field:value[0] error:failed to parse number; "
+            "field:value[1] error:failed to parse number]")
+      << test_struct.status();
+}
+
+//
+// Nested struct tests
+//
+
+TEST(JsonObjectLoader, NestedStructFields) {
+  struct NestedStruct {
+    int32_t inner = 0;
+
+    static const JsonLoaderInterface* JsonLoader(const JsonArgs&) {
+      static const auto* loader = JsonObjectLoader<NestedStruct>()
+                                      .Field("inner", &NestedStruct::inner)
+                                      .Finish();
+      return loader;
+    }
+  };
+  struct TestStruct {
+    NestedStruct outer;
+    NestedStruct optional_outer;
+    absl::optional<NestedStruct> absl_optional_outer;
+
+    static const JsonLoaderInterface* JsonLoader(const JsonArgs&) {
+      static const auto* loader =
+          JsonObjectLoader<TestStruct>()
+              .Field("outer", &TestStruct::outer)
+              .OptionalField("optional_outer", &TestStruct::optional_outer)
+              .OptionalField("absl_optional_outer",
+                             &TestStruct::absl_optional_outer)
+              .Finish();
+      return loader;
+    }
+  };
+  // Valid nested struct.
+  auto test_struct = Parse<TestStruct>("{\"outer\": {\"inner\": 1}}");
+  ASSERT_TRUE(test_struct.ok()) << test_struct.status();
+  EXPECT_EQ(test_struct->outer.inner, 1);
+  EXPECT_EQ(test_struct->optional_outer.inner, 0);
+  EXPECT_FALSE(test_struct->absl_optional_outer.has_value());
+  // Fails if required field is not present.
+  test_struct = Parse<TestStruct>("{}");
+  EXPECT_EQ(test_struct.status().code(), absl::StatusCode::kInvalidArgument);
+  EXPECT_EQ(test_struct.status().message(),
+            "errors validating JSON: [field:outer error:field not present]")
+      << test_struct.status();
+  // Fails if inner required field is not present.
+  test_struct = Parse<TestStruct>("{\"outer\": {}}");
+  EXPECT_EQ(test_struct.status().code(), absl::StatusCode::kInvalidArgument);
+  EXPECT_EQ(
+      test_struct.status().message(),
+      "errors validating JSON: [field:outer.inner error:field not present]")
+      << test_struct.status();
+  // Optional fields present.
+  test_struct = Parse<TestStruct>(
+      "{\"outer\": {\"inner\":1}, \"optional_outer\": {\"inner\":2}, "
+      "\"absl_optional_outer\": {\"inner\":3}}");
+  ASSERT_TRUE(test_struct.ok()) << test_struct.status();
+  EXPECT_EQ(test_struct->outer.inner, 1);
+  EXPECT_EQ(test_struct->optional_outer.inner, 2);
+  ASSERT_TRUE(test_struct->absl_optional_outer.has_value());
+  EXPECT_EQ(test_struct->absl_optional_outer->inner, 3);
+  // Wrong JSON type.
+  test_struct = Parse<TestStruct>(
+      "{\"outer\": \"foo\", \"optional_outer\": true, "
+      "\"absl_optional_outer\": 1}");
+  EXPECT_EQ(test_struct.status().code(), absl::StatusCode::kInvalidArgument);
+  EXPECT_EQ(test_struct.status().message(),
+            "errors validating JSON: ["
+            "field:absl_optional_outer error:is not an object; "
+            "field:optional_outer error:is not an object; "
+            "field:outer error:is not an object]")
+      << test_struct.status();
+  // Wrong JSON type for inner value.
+  test_struct = Parse<TestStruct>(
+      "{\"outer\": {\"inner\":\"foo\"}, \"optional_outer\": {\"inner\":true}, "
+      "\"absl_optional_outer\": {\"inner\":[]}}");
+  EXPECT_EQ(test_struct.status().code(), absl::StatusCode::kInvalidArgument);
+  EXPECT_EQ(test_struct.status().message(),
+            "errors validating JSON: ["
+            "field:absl_optional_outer.inner error:is not a number; "
+            "field:optional_outer.inner error:is not a number; "
+            "field:outer.inner error:failed to parse number]")
+      << test_struct.status();
+}
+
+TEST(JsonObjectLoader, BareString) {
+  auto parsed = Parse<std::string>("\"foo\"");
+  ASSERT_TRUE(parsed.ok()) << parsed.status();
+  EXPECT_EQ(*parsed, "foo");
+}
+
+TEST(JsonObjectLoader, BareDuration) {
+  auto parsed = Parse<Duration>("\"1.5s\"");
+  ASSERT_TRUE(parsed.ok()) << parsed.status();
+  EXPECT_EQ(*parsed, Duration::Milliseconds(1500));
+}
+
+TEST(JsonObjectLoader, BareSignedInteger) {
+  auto parsed = Parse<int32_t>("5");
+  ASSERT_TRUE(parsed.ok()) << parsed.status();
+  EXPECT_EQ(*parsed, 5);
+}
+
+TEST(JsonObjectLoader, BareUnsignedInteger) {
+  auto parsed = Parse<uint32_t>("5");
+  ASSERT_TRUE(parsed.ok()) << parsed.status();
+  EXPECT_EQ(*parsed, 5);
+}
+
+TEST(JsonObjectLoader, BareFloat) {
+  auto parsed = Parse<float>("5.2");
+  ASSERT_TRUE(parsed.ok()) << parsed.status();
+  EXPECT_NEAR(*parsed, 5.2, 0.001);
+}
+
+TEST(JsonObjectLoader, BareBool) {
+  auto parsed = Parse<bool>("true");
+  ASSERT_TRUE(parsed.ok()) << parsed.status();
+  EXPECT_TRUE(*parsed);
+}
+
+TEST(JsonObjectLoader, BareVector) {
+  auto parsed = Parse<std::vector<int32_t>>("[1, 2, 3]");
+  ASSERT_TRUE(parsed.ok()) << parsed.status();
+  EXPECT_THAT(*parsed, ::testing::ElementsAre(1, 2, 3));
+}
+
+TEST(JsonObjectLoader, BareMap) {
+  auto parsed =
+      Parse<std::map<std::string, int32_t>>("{\"a\":1, \"b\":2, \"c\":3}");
+  ASSERT_TRUE(parsed.ok()) << parsed.status();
+  EXPECT_THAT(*parsed, ::testing::ElementsAre(::testing::Pair("a", 1),
+                                              ::testing::Pair("b", 2),
+                                              ::testing::Pair("c", 3)));
+}
+
+TEST(JsonObjectLoader, IgnoresUnsupportedFields) {
+  struct TestStruct {
+    int32_t a = 0;
+
+    static const JsonLoaderInterface* JsonLoader(const JsonArgs&) {
+      static const auto* loader =
+          JsonObjectLoader<TestStruct>().Field("a", &TestStruct::a).Finish();
+      return loader;
+    }
+  };
+  auto test_struct = Parse<TestStruct>("{\"a\": 3, \"b\":false}");
+  ASSERT_TRUE(test_struct.ok()) << test_struct.status();
+  EXPECT_EQ(test_struct->a, 3);
+}
+
+TEST(JsonObjectLoader, IgnoresDisabledFields) {
+  class FakeJsonArgs : public JsonArgs {
+   public:
+    FakeJsonArgs() = default;
+
+    bool IsEnabled(absl::string_view key) const override {
+      return key != "disabled";
+    }
+  };
+  struct TestStruct {
+    int32_t a = 0;
+    int32_t b = 0;
+    int32_t c = 0;
+
+    static const JsonLoaderInterface* JsonLoader(const JsonArgs&) {
+      static const auto* loader =
+          JsonObjectLoader<TestStruct>()
+              .Field("a", &TestStruct::a, "disabled")
+              .OptionalField("b", &TestStruct::b, "disabled")
+              .OptionalField("c", &TestStruct::c, "enabled")
+              .Finish();
+      return loader;
+    }
+  };
+  // Fields "a" and "b" have the wrong types, but we ignore them,
+  // because they're disabled.
+  auto test_struct =
+      Parse<TestStruct>("{\"a\":false, \"b\":false, \"c\":1}", FakeJsonArgs());
+  ASSERT_TRUE(test_struct.ok()) << test_struct.status();
+  EXPECT_EQ(test_struct->a, 0);
+  EXPECT_EQ(test_struct->b, 0);
+  EXPECT_EQ(test_struct->c, 1);
+}
+
+TEST(JsonObjectLoader, PostLoadHook) {
+  struct TestStruct {
+    int32_t a = 0;
+
+    static const JsonLoaderInterface* JsonLoader(const JsonArgs&) {
+      static const auto* loader = JsonObjectLoader<TestStruct>()
+                                      .OptionalField("a", &TestStruct::a)
+                                      .Finish();
+      return loader;
+    }
+
+    void JsonPostLoad(const Json& /*source*/, const JsonArgs& /*args*/,
+                      ErrorList* /*errors*/) {
+      ++a;
+    }
+  };
+  auto test_struct = Parse<TestStruct>("{\"a\": 1}");
+  ASSERT_TRUE(test_struct.ok()) << test_struct.status();
+  EXPECT_EQ(test_struct->a, 2);
+  test_struct = Parse<TestStruct>("{}");
+  ASSERT_TRUE(test_struct.ok()) << test_struct.status();
+  EXPECT_EQ(test_struct->a, 1);
+}
+
+TEST(JsonObjectLoader, CustomValidationInPostLoadHook) {
+  struct TestStruct {
+    int32_t a = 0;
+
+    static const JsonLoaderInterface* JsonLoader(const JsonArgs&) {
+      static const auto* loader =
+          JsonObjectLoader<TestStruct>().Field("a", &TestStruct::a).Finish();
+      return loader;
+    }
+
+    void JsonPostLoad(const Json& /*source*/, const JsonArgs& /*args*/,
+                      ErrorList* errors) {
+      ScopedField field(errors, ".a");
+      if (!errors->FieldHasErrors() && a <= 0) {
+        errors->AddError("must be greater than 0");
+      }
+    }
+  };
+  // Value greater than 0.
+  auto test_struct = Parse<TestStruct>("{\"a\": 1}");
+  ASSERT_TRUE(test_struct.ok()) << test_struct.status();
+  EXPECT_EQ(test_struct->a, 1);
+  // Value 0, triggers custom validation.
+  test_struct = Parse<TestStruct>("{\"a\": 0}");
+  EXPECT_EQ(test_struct.status().code(), absl::StatusCode::kInvalidArgument);
+  EXPECT_EQ(test_struct.status().message(),
+            "errors validating JSON: [field:a error:must be greater than 0]")
+      << test_struct.status();
+  // Invalid type, generates built-in parsing error, so custom
+  // validation will not generate a new error.
+  test_struct = Parse<TestStruct>("{\"a\": []}");
+  EXPECT_EQ(test_struct.status().code(), absl::StatusCode::kInvalidArgument);
+  EXPECT_EQ(test_struct.status().message(),
+            "errors validating JSON: [field:a error:is not a number]")
+      << test_struct.status();
+}
+
+TEST(JsonObjectLoader, LoadFromJsonWithErrorList) {
+  struct TestStruct {
+    int32_t a = 0;
+
+    static const JsonLoaderInterface* JsonLoader(const JsonArgs&) {
+      static const auto* loader =
+          JsonObjectLoader<TestStruct>().Field("a", &TestStruct::a).Finish();
+      return loader;
+    }
+  };
+  // Valid.
+  {
+    absl::string_view json_str = "{\"a\":1}";
+    auto json = Json::Parse(json_str);
+    ASSERT_TRUE(json.ok()) << json.status();
+    ErrorList errors;
+    TestStruct test_struct =
+        LoadFromJson<TestStruct>(*json, JsonArgs(), &errors);
+    ASSERT_TRUE(errors.ok()) << errors.status();
+    EXPECT_EQ(test_struct.a, 1);
+  }
+  // Invalid.
+  {
+    absl::string_view json_str = "{\"a\":\"foo\"}";
+    auto json = Json::Parse(json_str);
+    ASSERT_TRUE(json.ok()) << json.status();
+    ErrorList errors;
+    LoadFromJson<TestStruct>(*json, JsonArgs(), &errors);
+    absl::Status status = errors.status();
+    EXPECT_EQ(status.code(), absl::StatusCode::kInvalidArgument);
+    EXPECT_EQ(status.message(),
+              "errors validating JSON: [field:a error:failed to parse number]")
+        << status;
+  }
+}
+
+TEST(JsonObjectLoader, LoadJsonObjectField) {
+  absl::string_view json_str = "{\"int\":1}";
+  auto json = Json::Parse(json_str);
+  ASSERT_TRUE(json.ok()) << json.status();
+  // Load a valid field.
+  {
+    ErrorList errors;
+    auto value = LoadJsonObjectField<int32_t>(json->object_value(), JsonArgs(),
+                                              "int", &errors);
+    ASSERT_TRUE(value.has_value()) << errors.status();
+    EXPECT_EQ(*value, 1);
+    EXPECT_TRUE(errors.ok());
+  }
+  // An optional field that is not present.
+  {
+    ErrorList errors;
+    auto value = LoadJsonObjectField<int32_t>(json->object_value(), JsonArgs(),
+                                              "not_present", &errors,
+                                              /*required=*/false);
+    EXPECT_FALSE(value.has_value());
+    EXPECT_TRUE(errors.ok());
+  }
+  // A required field that is not present.
+  {
+    ErrorList errors;
+    auto value = LoadJsonObjectField<int32_t>(json->object_value(), JsonArgs(),
+                                              "not_present", &errors);
+    EXPECT_FALSE(value.has_value());
+    auto status = errors.status();
+    EXPECT_THAT(status.code(), absl::StatusCode::kInvalidArgument);
+    EXPECT_EQ(status.message(),
+              "errors validating JSON: ["
+              "field:not_present error:field not present]")
+        << status;
+  }
+  // Value has the wrong type.
+  {
+    ErrorList errors;
+    auto value = LoadJsonObjectField<std::string>(json->object_value(),
+                                                  JsonArgs(), "int", &errors);
+    EXPECT_FALSE(value.has_value());
+    auto status = errors.status();
+    EXPECT_THAT(status.code(), absl::StatusCode::kInvalidArgument);
+    EXPECT_EQ(status.message(),
+              "errors validating JSON: [field:int error:is not a string]")
+        << status;
+  }
+}
+
+}  // namespace
+}  // namespace grpc_core
+
+int main(int argc, char** argv) {
+  ::testing::InitGoogleTest(&argc, argv);
+  return RUN_ALL_TESTS();
+}
diff --git a/tools/doxygen/Doxyfile.c++.internal b/tools/doxygen/Doxyfile.c++.internal
index 3d4ba13..65d7270 100644
--- a/tools/doxygen/Doxyfile.c++.internal
+++ b/tools/doxygen/Doxyfile.c++.internal
@@ -2208,6 +2208,9 @@
 src/core/lib/iomgr/wakeup_fd_posix.cc \
 src/core/lib/iomgr/wakeup_fd_posix.h \
 src/core/lib/json/json.h \
+src/core/lib/json/json_args.h \
+src/core/lib/json/json_object_loader.cc \
+src/core/lib/json/json_object_loader.h \
 src/core/lib/json/json_reader.cc \
 src/core/lib/json/json_util.cc \
 src/core/lib/json/json_util.h \
diff --git a/tools/doxygen/Doxyfile.core.internal b/tools/doxygen/Doxyfile.core.internal
index 08c036c..e89a845 100644
--- a/tools/doxygen/Doxyfile.core.internal
+++ b/tools/doxygen/Doxyfile.core.internal
@@ -2001,6 +2001,9 @@
 src/core/lib/iomgr/wakeup_fd_posix.cc \
 src/core/lib/iomgr/wakeup_fd_posix.h \
 src/core/lib/json/json.h \
+src/core/lib/json/json_args.h \
+src/core/lib/json/json_object_loader.cc \
+src/core/lib/json/json_object_loader.h \
 src/core/lib/json/json_reader.cc \
 src/core/lib/json/json_util.cc \
 src/core/lib/json/json_util.h \
diff --git a/tools/run_tests/generated/tests.json b/tools/run_tests/generated/tests.json
index 028e370..15ea62f 100644
--- a/tools/run_tests/generated/tests.json
+++ b/tools/run_tests/generated/tests.json
@@ -4178,6 +4178,30 @@
     "flaky": false,
     "gtest": true,
     "language": "c++",
+    "name": "json_object_loader_test",
+    "platforms": [
+      "linux",
+      "mac",
+      "posix",
+      "windows"
+    ],
+    "uses_polling": false
+  },
+  {
+    "args": [],
+    "benchmark": false,
+    "ci_platforms": [
+      "linux",
+      "mac",
+      "posix",
+      "windows"
+    ],
+    "cpu_cost": 1.0,
+    "exclude_configs": [],
+    "exclude_iomgrs": [],
+    "flaky": false,
+    "gtest": true,
+    "language": "c++",
     "name": "json_test",
     "platforms": [
       "linux",