diff --git a/xds-api/src/main/java/com/linecorp/armeria/xds/api/IgnoreUnsupportedFieldHandler.java b/xds-api/src/main/java/com/linecorp/armeria/xds/api/IgnoreUnsupportedFieldHandler.java
new file mode 100644
index 00000000000..d45f9c45ff5
--- /dev/null
+++ b/xds-api/src/main/java/com/linecorp/armeria/xds/api/IgnoreUnsupportedFieldHandler.java
@@ -0,0 +1,26 @@
+/*
+ * Copyright 2026 LY Corporation
+ *
+ * LY Corporation licenses this file to you 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:
+ *
+ * https://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.
+ */
+
+package com.linecorp.armeria.xds.api;
+
+enum IgnoreUnsupportedFieldHandler implements UnsupportedFieldHandler {
+ INSTANCE;
+
+ @Override
+ public void handle(String descriptorName, String fieldPath, Object value) {
+ // no-op
+ }
+}
diff --git a/xds-api/src/main/java/com/linecorp/armeria/xds/api/SupportedFieldValidator.java b/xds-api/src/main/java/com/linecorp/armeria/xds/api/SupportedFieldValidator.java
new file mode 100644
index 00000000000..3e31b0a3614
--- /dev/null
+++ b/xds-api/src/main/java/com/linecorp/armeria/xds/api/SupportedFieldValidator.java
@@ -0,0 +1,183 @@
+/*
+ * Copyright 2026 LY Corporation
+ *
+ * LY Corporation licenses this file to you 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:
+ *
+ * https://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.
+ */
+
+package com.linecorp.armeria.xds.api;
+
+import static java.util.Objects.requireNonNull;
+
+import java.util.Collections;
+import java.util.HashSet;
+import java.util.List;
+import java.util.Map;
+import java.util.Set;
+import java.util.concurrent.ConcurrentHashMap;
+import java.util.concurrent.ConcurrentMap;
+
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+import com.google.protobuf.Descriptors;
+import com.google.protobuf.Descriptors.EnumValueDescriptor;
+import com.google.protobuf.Descriptors.FieldDescriptor;
+import com.google.protobuf.Message;
+
+import com.linecorp.armeria.common.annotation.UnstableApi;
+
+/**
+ * Validates protobuf messages against the {@code (armeria.xds.supported)} field annotation.
+ * Any set field that lacks the annotation is reported as an unsupported field violation.
+ * The validator walks recursively into supported message-typed fields.
+ *
+ *
Currently, supported fields are annotated inline on each field declaration in the proto files, e.g.:
+ *
{@code
+ * string exact = 1 [(armeria.xds.supported) = true];
+ * }
+ */
+@UnstableApi
+public final class SupportedFieldValidator {
+
+ static final Logger unsupportedLogger = LoggerFactory.getLogger("com.linecorp.armeria.xds.unsupported");
+ private static final SupportedFieldValidator DEFAULT = new SupportedFieldValidator(
+ UnsupportedFieldHandler.warn());
+ private static final SupportedFieldValidator NOOP = new SupportedFieldValidator(
+ UnsupportedFieldHandler.ignore());
+
+ private final ConcurrentMap> supportedFieldsCache =
+ new ConcurrentHashMap<>();
+
+ private final UnsupportedFieldHandler handler;
+
+ private SupportedFieldValidator(UnsupportedFieldHandler handler) {
+ this.handler = requireNonNull(handler, "handler");
+ }
+
+ /**
+ * Returns a {@link SupportedFieldValidator} with the default {@link UnsupportedFieldHandler#warn()}
+ * handler.
+ */
+ public static SupportedFieldValidator of() {
+ return DEFAULT;
+ }
+
+ /**
+ * Returns a {@link SupportedFieldValidator} with the specified {@link UnsupportedFieldHandler}.
+ */
+ public static SupportedFieldValidator of(UnsupportedFieldHandler handler) {
+ requireNonNull(handler, "handler");
+ if (handler == IgnoreUnsupportedFieldHandler.INSTANCE) {
+ return NOOP;
+ }
+ return new SupportedFieldValidator(handler);
+ }
+
+ /**
+ * Returns a no-op validator that does not perform any validation.
+ */
+ public static SupportedFieldValidator noop() {
+ return NOOP;
+ }
+
+ /**
+ * Validates the message, calling the handler directly for each unsupported field found.
+ * If the handler is the {@link UnsupportedFieldHandler#ignore()} sentinel, returns immediately
+ * to skip recursion cost.
+ */
+ public void validate(Message message) {
+ requireNonNull(message, "message");
+ if (handler == IgnoreUnsupportedFieldHandler.INSTANCE) {
+ return;
+ }
+ final String descriptorName = message.getDescriptorForType().getFullName();
+ doValidate(message, descriptorName, "$");
+ }
+
+ @SuppressWarnings("unchecked")
+ private void doValidate(Message message, String descriptorName, String path) {
+ if (unsupportedPackage(message.getDescriptorForType().getFile().getPackage())) {
+ return;
+ }
+ final Descriptors.Descriptor descriptor = message.getDescriptorForType();
+ final Set supported = supportedFields(descriptor);
+
+ for (Map.Entry entry : message.getAllFields().entrySet()) {
+ final FieldDescriptor fd = entry.getKey();
+ final Object value = entry.getValue();
+ final String fieldPath = path + '.' + fd.getJsonName();
+
+ if (!supported.contains(fd)) {
+ handler.handle(descriptorName, fieldPath, value);
+ continue;
+ }
+
+ // Field is supported — check enum values and recurse into nested messages.
+ if (fd.isMapField()) {
+ final FieldDescriptor valueField = fd.getMessageType().findFieldByNumber(2);
+ if (valueField != null) {
+ final List mapEntries = (List) value;
+ for (int i = 0; i < mapEntries.size(); i++) {
+ validateFieldValue(valueField, mapEntries.get(i).getField(valueField),
+ descriptorName, fieldPath + '[' + i + "].value");
+ }
+ }
+ } else if (fd.isRepeated()) {
+ final List> elements = (List>) value;
+ for (int i = 0; i < elements.size(); i++) {
+ validateFieldValue(fd, elements.get(i), descriptorName,
+ fieldPath + '[' + i + ']');
+ }
+ } else {
+ validateFieldValue(fd, value, descriptorName, fieldPath);
+ }
+ }
+ }
+
+ private void validateFieldValue(FieldDescriptor fd, Object value,
+ String descriptorName, String fieldPath) {
+ if (fd.getJavaType() == FieldDescriptor.JavaType.MESSAGE) {
+ if (value instanceof Message) {
+ doValidate((Message) value, descriptorName, fieldPath);
+ }
+ } else if (fd.getJavaType() == FieldDescriptor.JavaType.ENUM) {
+ if (!unsupportedPackage(fd.getEnumType().getFile().getPackage()) &&
+ value instanceof EnumValueDescriptor) {
+ final EnumValueDescriptor ev = (EnumValueDescriptor) value;
+ if (unsupportedEnumValue(ev)) {
+ handler.handle(descriptorName, fieldPath, ev);
+ }
+ }
+ }
+ }
+
+ private static boolean unsupportedEnumValue(EnumValueDescriptor ev) {
+ return !ev.getOptions().getExtension(SupportedFieldProto.supportedValue);
+ }
+
+ private static boolean unsupportedPackage(String pkg) {
+ return !(pkg.startsWith("envoy.") || pkg.startsWith("xds.") || pkg.startsWith("armeria."));
+ }
+
+ private Set supportedFields(Descriptors.Descriptor descriptor) {
+ return supportedFieldsCache.computeIfAbsent(descriptor, d -> {
+ final Set result = new HashSet<>();
+ for (FieldDescriptor fd : d.getFields()) {
+ if (fd.getOptions().getExtension(SupportedFieldProto.supported)) {
+ result.add(fd);
+ }
+ }
+ return Collections.unmodifiableSet(result);
+ });
+ }
+}
diff --git a/xds-api/src/main/java/com/linecorp/armeria/xds/api/UnsupportedFieldHandler.java b/xds-api/src/main/java/com/linecorp/armeria/xds/api/UnsupportedFieldHandler.java
new file mode 100644
index 00000000000..93b02eff63b
--- /dev/null
+++ b/xds-api/src/main/java/com/linecorp/armeria/xds/api/UnsupportedFieldHandler.java
@@ -0,0 +1,77 @@
+/*
+ * Copyright 2026 LY Corporation
+ *
+ * LY Corporation licenses this file to you 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:
+ *
+ * https://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.
+ */
+
+package com.linecorp.armeria.xds.api;
+
+import static com.linecorp.armeria.xds.api.SupportedFieldValidator.unsupportedLogger;
+import static java.util.Objects.requireNonNull;
+
+import com.linecorp.armeria.common.annotation.UnstableApi;
+
+/**
+ * A handler that is invoked when unsupported xDS fields are detected in a protobuf message.
+ * Unsupported fields are those not annotated with {@code (armeria.xds.supported) = true}.
+ */
+@UnstableApi
+@FunctionalInterface
+public interface UnsupportedFieldHandler {
+
+ /**
+ * Called when an unsupported field is detected.
+ *
+ * @param descriptorName the full name of the root message being validated
+ * (e.g., {@code "envoy.config.cluster.v3.Cluster"})
+ * @param fieldPath the JSON path of the unsupported field (e.g., {@code "$.edsConfig.serviceName"})
+ * @param value the raw value of the unsupported field
+ */
+ void handle(String descriptorName, String fieldPath, Object value);
+
+ /**
+ * Returns a composed handler that first invokes this handler, then the {@code after} handler.
+ */
+ default UnsupportedFieldHandler andThen(UnsupportedFieldHandler after) {
+ requireNonNull(after, "after");
+ return (descriptorName, fieldPath, value) -> {
+ handle(descriptorName, fieldPath, value);
+ after.handle(descriptorName, fieldPath, value);
+ };
+ }
+
+ /**
+ * Returns a handler that logs a warning for each unsupported field path.
+ */
+ static UnsupportedFieldHandler warn() {
+ return (descriptorName, fieldPath, value) ->
+ unsupportedLogger.warn("Unsupported xDS field detected in {}: {}", descriptorName, fieldPath);
+ }
+
+ /**
+ * Returns a handler that throws an {@link IllegalArgumentException} on the first unsupported field.
+ */
+ static UnsupportedFieldHandler reject() {
+ return (descriptorName, fieldPath, value) -> {
+ throw new IllegalArgumentException(
+ "Unsupported xDS field detected in " + descriptorName + ": " + fieldPath);
+ };
+ }
+
+ /**
+ * Returns a handler that silently ignores unsupported fields.
+ */
+ static UnsupportedFieldHandler ignore() {
+ return IgnoreUnsupportedFieldHandler.INSTANCE;
+ }
+}
diff --git a/xds-api/src/main/proto/armeria/xds/supported.proto b/xds-api/src/main/proto/armeria/xds/supported.proto
new file mode 100644
index 00000000000..ba594135ca6
--- /dev/null
+++ b/xds-api/src/main/proto/armeria/xds/supported.proto
@@ -0,0 +1,16 @@
+syntax = "proto3";
+package armeria.xds;
+
+option java_package = "com.linecorp.armeria.xds.api";
+option java_outer_classname = "SupportedFieldProto";
+
+import "google/protobuf/descriptor.proto";
+
+extend google.protobuf.FieldOptions {
+ optional bool supported = 50000;
+}
+
+extend google.protobuf.EnumValueOptions {
+ // Distinct name from the FieldOptions extension to avoid Java codegen name collision.
+ optional bool supported_value = 50000;
+}
diff --git a/xds-api/src/test/java/com/linecorp/armeria/xds/api/SupportedFieldValidatorTest.java b/xds-api/src/test/java/com/linecorp/armeria/xds/api/SupportedFieldValidatorTest.java
new file mode 100644
index 00000000000..1f5c7bc1d05
--- /dev/null
+++ b/xds-api/src/test/java/com/linecorp/armeria/xds/api/SupportedFieldValidatorTest.java
@@ -0,0 +1,297 @@
+/*
+ * Copyright 2026 LY Corporation
+ *
+ * LY Corporation licenses this file to you 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:
+ *
+ * https://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.
+ */
+
+package com.linecorp.armeria.xds.api;
+
+import static org.assertj.core.api.Assertions.assertThat;
+import static org.assertj.core.api.Assertions.assertThatThrownBy;
+
+import java.util.ArrayList;
+import java.util.List;
+
+import org.junit.jupiter.api.Test;
+
+import com.google.protobuf.Any;
+import com.google.protobuf.Descriptors.EnumValueDescriptor;
+import com.google.protobuf.Duration;
+import com.google.protobuf.UInt32Value;
+
+import com.linecorp.armeria.xds.api.testing.TestCluster;
+import com.linecorp.armeria.xds.api.testing.TestDiscoveryType;
+import com.linecorp.armeria.xds.api.testing.TestEdsConfig;
+import com.linecorp.armeria.xds.api.testing.TestOutlierDetection;
+
+class SupportedFieldValidatorTest {
+
+ @Test
+ void allSupportedFields() {
+ final List violations = new ArrayList<>();
+ final SupportedFieldValidator validator =
+ SupportedFieldValidator.of((descriptor, path, value) -> violations.add(path));
+
+ final TestCluster cluster = TestCluster.newBuilder()
+ .setName("test")
+ .setType(TestDiscoveryType.STATIC)
+ .build();
+ validator.validate(cluster);
+ assertThat(violations).isEmpty();
+ }
+
+ @Test
+ void unsupportedScalarField() {
+ final List violations = new ArrayList<>();
+ final List values = new ArrayList<>();
+ final SupportedFieldValidator validator =
+ SupportedFieldValidator.of((descriptor, path, value) -> {
+ violations.add(path);
+ values.add(value);
+ });
+
+ final TestCluster cluster = TestCluster.newBuilder()
+ .setName("test")
+ .setUnsupportedField("bad")
+ .build();
+ validator.validate(cluster);
+ assertThat(violations).containsExactly("$.unsupportedField");
+ assertThat(values).containsExactly("bad");
+ }
+
+ @Test
+ void unsupportedMessageField() {
+ final List violations = new ArrayList<>();
+ final List values = new ArrayList<>();
+ final SupportedFieldValidator validator =
+ SupportedFieldValidator.of((descriptor, path, value) -> {
+ violations.add(path);
+ values.add(value);
+ });
+
+ final TestOutlierDetection outlier = TestOutlierDetection.newBuilder()
+ .setConsecutiveErrors(5)
+ .build();
+ final TestCluster cluster = TestCluster.newBuilder()
+ .setName("test")
+ .setOutlierDetection(outlier)
+ .build();
+ validator.validate(cluster);
+ assertThat(violations).containsExactly("$.outlierDetection");
+ assertThat(values).containsExactly(outlier);
+ }
+
+ @Test
+ void recursiveNestedUnsupported() {
+ final List violations = new ArrayList<>();
+ final List values = new ArrayList<>();
+ final SupportedFieldValidator validator =
+ SupportedFieldValidator.of((descriptor, path, value) -> {
+ violations.add(path);
+ values.add(value);
+ });
+
+ final TestCluster cluster = TestCluster.newBuilder()
+ .setName("test")
+ .setEdsConfig(
+ TestEdsConfig.newBuilder()
+ .setServiceName("svc")
+ .setUnsupportedNested("bad")
+ .build())
+ .build();
+ validator.validate(cluster);
+ assertThat(violations).containsExactly("$.edsConfig.unsupportedNested");
+ assertThat(values).containsExactly("bad");
+ }
+
+ @Test
+ void unsupportedEnumValue() {
+ final List violations = new ArrayList<>();
+ final List values = new ArrayList<>();
+ final SupportedFieldValidator validator =
+ SupportedFieldValidator.of((descriptor, path, value) -> {
+ violations.add(path);
+ values.add(value);
+ });
+
+ final TestCluster cluster = TestCluster.newBuilder()
+ .setName("test")
+ .setType(TestDiscoveryType.LOGICAL_DNS)
+ .build();
+ validator.validate(cluster);
+ assertThat(violations).containsExactly("$.type");
+ assertThat(values).hasSize(1);
+ assertThat(((EnumValueDescriptor) values.get(0)).getName()).isEqualTo("LOGICAL_DNS");
+ }
+
+ @Test
+ void rejectHandler() {
+ final SupportedFieldValidator validator = SupportedFieldValidator.of(
+ UnsupportedFieldHandler.reject());
+
+ final TestCluster cluster = TestCluster.newBuilder()
+ .setName("test")
+ .setUnsupportedField("bad")
+ .build();
+ assertThatThrownBy(() -> validator.validate(cluster))
+ .isInstanceOf(IllegalArgumentException.class)
+ .hasMessageContaining("$.unsupportedField");
+ }
+
+ @Test
+ void ignoreHandler() {
+ final SupportedFieldValidator validator = SupportedFieldValidator.of(
+ UnsupportedFieldHandler.ignore());
+
+ final TestCluster cluster = TestCluster.newBuilder()
+ .setName("test")
+ .setUnsupportedField("bad")
+ .build();
+ // Should not throw
+ validator.validate(cluster);
+ }
+
+ @Test
+ void fullyUnannotatedMessage() {
+ final List violations = new ArrayList<>();
+ final SupportedFieldValidator validator =
+ SupportedFieldValidator.of((descriptor, path, value) -> violations.add(path));
+
+ final TestOutlierDetection outlier = TestOutlierDetection.newBuilder()
+ .setConsecutiveErrors(5)
+ .build();
+ validator.validate(outlier);
+ assertThat(violations).containsExactly("$.consecutiveErrors");
+ }
+
+ @Test
+ void noopValidatorDoesNothing() {
+ final SupportedFieldValidator validator = SupportedFieldValidator.noop();
+ final TestCluster cluster = TestCluster.newBuilder()
+ .setName("test")
+ .setUnsupportedField("bad")
+ .build();
+ // Should not throw - noop ignores everything
+ validator.validate(cluster);
+ }
+
+ @Test
+ void andThenComposition() {
+ final List first = new ArrayList<>();
+ final List second = new ArrayList<>();
+ final UnsupportedFieldHandler composed =
+ ((UnsupportedFieldHandler) (descriptor, path, value) -> first.add(path))
+ .andThen((descriptor, path, value) -> second.add(path));
+ final SupportedFieldValidator validator = SupportedFieldValidator.of(composed);
+
+ final TestCluster cluster = TestCluster.newBuilder()
+ .setName("test")
+ .setUnsupportedField("bad")
+ .build();
+ validator.validate(cluster);
+ assertThat(first).containsExactly("$.unsupportedField");
+ assertThat(second).containsExactly("$.unsupportedField");
+ }
+
+ @Test
+ void rejectFailsFast() {
+ final UnsupportedFieldHandler failFast = UnsupportedFieldHandler.reject();
+ final SupportedFieldValidator validator = SupportedFieldValidator.of(failFast);
+
+ final TestCluster cluster = TestCluster.newBuilder()
+ .setName("test")
+ .setUnsupportedField("bad")
+ .setOutlierDetection(
+ TestOutlierDetection.newBuilder()
+ .setConsecutiveErrors(5)
+ .build())
+ .build();
+ assertThatThrownBy(() -> validator.validate(cluster))
+ .isInstanceOf(IllegalArgumentException.class);
+ }
+
+ @Test
+ void ignoreEarlyExit() {
+ // Validate that ignore() handler causes early exit (no recursion)
+ // This is a behavior test — the ignore handler should skip validation entirely
+ final SupportedFieldValidator validator = SupportedFieldValidator.of(
+ UnsupportedFieldHandler.ignore());
+ final TestCluster cluster = TestCluster.newBuilder()
+ .setName("test")
+ .setUnsupportedField("bad")
+ .setOutlierDetection(
+ TestOutlierDetection.newBuilder()
+ .setConsecutiveErrors(5)
+ .build())
+ .build();
+ // Should not throw — ignore skips all validation
+ validator.validate(cluster);
+ }
+
+ @Test
+ void unsupportedEnumValueInMapField() {
+ final List violations = new ArrayList<>();
+ final List values = new ArrayList<>();
+ final SupportedFieldValidator validator =
+ SupportedFieldValidator.of((descriptor, path, value) -> {
+ violations.add(path);
+ values.add(value);
+ });
+
+ final TestCluster cluster = TestCluster.newBuilder()
+ .setName("test")
+ .putTypeMap("a", TestDiscoveryType.STATIC)
+ .putTypeMap("b", TestDiscoveryType.LOGICAL_DNS)
+ .build();
+ validator.validate(cluster);
+ assertThat(violations).hasSize(1);
+ assertThat(violations.get(0)).endsWith(".value");
+ assertThat(((EnumValueDescriptor) values.get(0)).getName()).isEqualTo("LOGICAL_DNS");
+ }
+
+ @Test
+ void supportedEnumValueInMapField() {
+ final List violations = new ArrayList<>();
+ final SupportedFieldValidator validator =
+ SupportedFieldValidator.of((descriptor, path, value) -> violations.add(path));
+
+ final TestCluster cluster = TestCluster.newBuilder()
+ .setName("test")
+ .putTypeMap("a", TestDiscoveryType.STATIC)
+ .putTypeMap("b", TestDiscoveryType.EDS)
+ .build();
+ validator.validate(cluster);
+ assertThat(violations).isEmpty();
+ }
+
+ @Test
+ void externalTypesDoNotProduceFalsePositives() {
+ final List violations = new ArrayList<>();
+ final SupportedFieldValidator validator =
+ SupportedFieldValidator.of((descriptor, path, value) -> violations.add(path));
+
+ final TestCluster cluster = TestCluster.newBuilder()
+ .setName("test")
+ .setTypedConfig(Any.pack(
+ UInt32Value.of(42)))
+ .setMaxRequests(UInt32Value.of(100))
+ .setTimeout(Duration.newBuilder()
+ .setSeconds(30)
+ .build())
+ .build();
+ validator.validate(cluster);
+ // External types (Any, UInt32Value, Duration) should not be recursed into,
+ // so their internal fields (type_url, value, seconds, nanos) must not appear.
+ assertThat(violations).isEmpty();
+ }
+}
diff --git a/xds-api/src/test/proto/armeria/xds/testing/test_supported.proto b/xds-api/src/test/proto/armeria/xds/testing/test_supported.proto
new file mode 100644
index 00000000000..318d622633f
--- /dev/null
+++ b/xds-api/src/test/proto/armeria/xds/testing/test_supported.proto
@@ -0,0 +1,37 @@
+syntax = "proto3";
+package armeria.xds.testing;
+
+option java_package = "com.linecorp.armeria.xds.api.testing";
+option java_multiple_files = true;
+
+import "armeria/xds/supported.proto";
+import "google/protobuf/any.proto";
+import "google/protobuf/wrappers.proto";
+import "google/protobuf/duration.proto";
+
+message TestCluster {
+ string name = 1 [(armeria.xds.supported) = true];
+ TestDiscoveryType type = 2 [(armeria.xds.supported) = true];
+ TestEdsConfig eds_config = 3 [(armeria.xds.supported) = true];
+ string unsupported_field = 4;
+ TestOutlierDetection outlier_detection = 5;
+ google.protobuf.Any typed_config = 6 [(armeria.xds.supported) = true];
+ google.protobuf.UInt32Value max_requests = 7 [(armeria.xds.supported) = true];
+ google.protobuf.Duration timeout = 8 [(armeria.xds.supported) = true];
+ map type_map = 9 [(armeria.xds.supported) = true];
+}
+
+enum TestDiscoveryType {
+ STATIC = 0 [(armeria.xds.supported_value) = true];
+ EDS = 1 [(armeria.xds.supported_value) = true];
+ LOGICAL_DNS = 2;
+}
+
+message TestEdsConfig {
+ string service_name = 1 [(armeria.xds.supported) = true];
+ string unsupported_nested = 2;
+}
+
+message TestOutlierDetection {
+ int32 consecutive_errors = 1;
+}
diff --git a/xds-validator/src/main/java/com/linecorp/armeria/xds/validator/XdsValidatorIndex.java b/xds-validator/src/main/java/com/linecorp/armeria/xds/validator/XdsValidatorIndex.java
index dd5b3fbe3e1..957f9d615b4 100644
--- a/xds-validator/src/main/java/com/linecorp/armeria/xds/validator/XdsValidatorIndex.java
+++ b/xds-validator/src/main/java/com/linecorp/armeria/xds/validator/XdsValidatorIndex.java
@@ -1,5 +1,5 @@
/*
- * Copyright 2025 LY Corporation
+ * Copyright 2026 LY Corporation
*
* LY Corporation licenses this file to you under the Apache License,
* version 2.0 (the "License"); you may not use this file except in compliance
@@ -21,6 +21,7 @@
/**
* Validates an xDS resource. Validators are loaded using Java SPI (Service Provider Interface).
*/
+@FunctionalInterface
@UnstableApi
public interface XdsValidatorIndex {