diff --git a/common/internal/BUILD.bazel b/common/internal/BUILD.bazel
index 781566713..7c33e56b9 100644
--- a/common/internal/BUILD.bazel
+++ b/common/internal/BUILD.bazel
@@ -147,3 +147,8 @@ cel_android_library(
name = "date_time_helpers_android",
exports = ["//common/src/main/java/dev/cel/common/internal:date_time_helpers_android"],
)
+
+java_library(
+ name = "reflection_util",
+ exports = ["//common/src/main/java/dev/cel/common/internal:reflection_util"],
+)
diff --git a/common/src/main/java/dev/cel/common/internal/BUILD.bazel b/common/src/main/java/dev/cel/common/internal/BUILD.bazel
index 6b470d98c..58b15b103 100644
--- a/common/src/main/java/dev/cel/common/internal/BUILD.bazel
+++ b/common/src/main/java/dev/cel/common/internal/BUILD.bazel
@@ -398,8 +398,11 @@ java_library(
java_library(
name = "reflection_util",
srcs = ["ReflectionUtil.java"],
+ tags = [
+ ],
deps = [
"//common/annotations",
+ "@maven//:com_google_guava_guava",
],
)
diff --git a/common/src/main/java/dev/cel/common/internal/ReflectionUtil.java b/common/src/main/java/dev/cel/common/internal/ReflectionUtil.java
index e513a446b..57c06e311 100644
--- a/common/src/main/java/dev/cel/common/internal/ReflectionUtil.java
+++ b/common/src/main/java/dev/cel/common/internal/ReflectionUtil.java
@@ -14,9 +14,14 @@
package dev.cel.common.internal;
+import com.google.common.reflect.TypeToken;
import dev.cel.common.annotations.Internal;
import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Method;
+import java.lang.reflect.Type;
+import java.util.List;
+import java.util.Map;
+import java.util.Optional;
/**
* Utility class for invoking Java reflection.
@@ -48,5 +53,35 @@ public static Object invoke(Method method, Object object, Object... params) {
}
}
+ /**
+ * Extracts the element type of a container type (List, Map, or Optional). Returns the type itself
+ * if it's not a container or if generic type info is missing.
+ */
+ public static Class> getElementType(Class> type, Type genericType) {
+ TypeToken> token = TypeToken.of(genericType);
+
+ if (List.class.isAssignableFrom(type)) {
+ return token.resolveType(List.class.getTypeParameters()[0]).getRawType();
+ }
+ if (Map.class.isAssignableFrom(type)) {
+ return token.resolveType(Map.class.getTypeParameters()[1]).getRawType();
+ }
+ // Optional is a final class, so reference equality is equivalent to isAssignableFrom
+ // but slightly more performant than tree traversal.
+ if (type == Optional.class) {
+ return token.resolveType(Optional.class.getTypeParameters()[0]).getRawType();
+ }
+
+ return type;
+ }
+
+ /**
+ * Extracts the raw Class from a Type. Handles Class, ParameterizedType, and WildcardType (returns
+ * upper bound). Returns Object.class as fallback.
+ */
+ public static Class> getRawType(Type type) {
+ return TypeToken.of(type).getRawType();
+ }
+
private ReflectionUtil() {}
}
diff --git a/extensions/BUILD.bazel b/extensions/BUILD.bazel
index c6a029106..dea4cd760 100644
--- a/extensions/BUILD.bazel
+++ b/extensions/BUILD.bazel
@@ -56,3 +56,8 @@ java_library(
name = "comprehensions",
exports = ["//extensions/src/main/java/dev/cel/extensions:comprehensions"],
)
+
+java_library(
+ name = "native",
+ exports = ["//extensions/src/main/java/dev/cel/extensions:native"],
+)
diff --git a/extensions/src/main/java/dev/cel/extensions/BUILD.bazel b/extensions/src/main/java/dev/cel/extensions/BUILD.bazel
index f8e4bfc8c..73bab08c9 100644
--- a/extensions/src/main/java/dev/cel/extensions/BUILD.bazel
+++ b/extensions/src/main/java/dev/cel/extensions/BUILD.bazel
@@ -34,6 +34,7 @@ java_library(
":encoders",
":lists",
":math",
+ ":native",
":optional_library",
":protos",
":regex",
@@ -185,6 +186,7 @@ java_library(
"//common/types",
"//common/values",
"//common/values:cel_byte_string",
+ "//common/values:cel_value",
"//compiler:compiler_builder",
"//extensions:extension_library",
"//parser:macro",
@@ -318,3 +320,26 @@ java_library(
"@maven//:com_google_guava_guava",
],
)
+
+java_library(
+ name = "native",
+ srcs = ["CelNativeTypesExtensions.java"],
+ tags = [
+ ],
+ deps = [
+ "//checker:checker_builder",
+ "//common/exceptions:attribute_not_found",
+ "//common/internal:reflection_util",
+ "//common/types",
+ "//common/types:type_providers",
+ "//common/values",
+ "//common/values:cel_byte_string",
+ "//common/values:cel_value",
+ "//common/values:cel_value_provider",
+ "//compiler:compiler_builder",
+ "//runtime",
+ "@maven//:com_google_errorprone_error_prone_annotations",
+ "@maven//:com_google_guava_guava",
+ "@maven//:org_jspecify_jspecify",
+ ],
+)
diff --git a/extensions/src/main/java/dev/cel/extensions/CelExtensions.java b/extensions/src/main/java/dev/cel/extensions/CelExtensions.java
index 8f1770f3f..8adc39384 100644
--- a/extensions/src/main/java/dev/cel/extensions/CelExtensions.java
+++ b/extensions/src/main/java/dev/cel/extensions/CelExtensions.java
@@ -15,13 +15,13 @@
package dev.cel.extensions;
import static com.google.common.collect.ImmutableSet.toImmutableSet;
-import static java.util.Arrays.stream;
import com.google.common.collect.ImmutableSet;
import com.google.common.collect.Streams;
import com.google.errorprone.annotations.InlineMe;
import dev.cel.common.CelOptions;
import dev.cel.extensions.CelMathExtensions.Function;
+import java.util.EnumSet;
import java.util.Set;
/**
@@ -350,6 +350,18 @@ public static CelComprehensionsExtensions comprehensions() {
return COMPREHENSIONS_EXTENSIONS;
}
+ /**
+ * Extensions for supporting native Java types (POJOs) in CEL.
+ *
+ *
Refer to README.md for details on property discovery, type mapping, and limitations.
+ *
+ *
Note: Passing classes with unsupported types or anonymous/local classes will result in an
+ * {@link IllegalArgumentException} when the runtime is built.
+ */
+ public static CelNativeTypesExtensions nativeTypes(Class>... classes) {
+ return CelNativeTypesExtensions.nativeTypes(classes);
+ }
+
/**
* Retrieves all function names used by every extension libraries.
*
@@ -359,18 +371,17 @@ public static CelComprehensionsExtensions comprehensions() {
*/
public static ImmutableSet getAllFunctionNames() {
return Streams.concat(
- stream(CelMathExtensions.Function.values())
- .map(CelMathExtensions.Function::getFunction),
- stream(CelStringExtensions.Function.values())
+ EnumSet.allOf(Function.class).stream().map(CelMathExtensions.Function::getFunction),
+ EnumSet.allOf(CelStringExtensions.Function.class).stream()
.map(CelStringExtensions.Function::getFunction),
- stream(SetsFunction.values()).map(SetsFunction::getFunction),
- stream(CelEncoderExtensions.Function.values())
+ EnumSet.allOf(SetsFunction.class).stream().map(SetsFunction::getFunction),
+ EnumSet.allOf(CelEncoderExtensions.Function.class).stream()
.map(CelEncoderExtensions.Function::getFunction),
- stream(CelListsExtensions.Function.values())
+ EnumSet.allOf(CelListsExtensions.Function.class).stream()
.map(CelListsExtensions.Function::getFunction),
- stream(CelRegexExtensions.Function.values())
+ EnumSet.allOf(CelRegexExtensions.Function.class).stream()
.map(CelRegexExtensions.Function::getFunction),
- stream(CelComprehensionsExtensions.Function.values())
+ EnumSet.allOf(CelComprehensionsExtensions.Function.class).stream()
.map(CelComprehensionsExtensions.Function::getFunction))
.collect(toImmutableSet());
}
diff --git a/extensions/src/main/java/dev/cel/extensions/CelNativeTypesExtensions.java b/extensions/src/main/java/dev/cel/extensions/CelNativeTypesExtensions.java
new file mode 100644
index 000000000..8ea836064
--- /dev/null
+++ b/extensions/src/main/java/dev/cel/extensions/CelNativeTypesExtensions.java
@@ -0,0 +1,1050 @@
+// Copyright 2026 Google LLC
+//
+// 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
+//
+// 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 dev.cel.extensions;
+
+import static com.google.common.collect.ImmutableSet.toImmutableSet;
+import static java.util.Arrays.stream;
+
+import com.google.common.annotations.VisibleForTesting;
+import com.google.common.base.Preconditions;
+import com.google.common.collect.ImmutableList;
+import com.google.common.collect.ImmutableMap;
+import com.google.common.collect.ImmutableSet;
+import com.google.common.primitives.Primitives;
+import com.google.common.primitives.UnsignedLong;
+import com.google.common.reflect.TypeToken;
+import com.google.errorprone.annotations.Immutable;
+import dev.cel.checker.CelCheckerBuilder;
+import dev.cel.common.exceptions.CelAttributeNotFoundException;
+import dev.cel.common.internal.ReflectionUtil;
+import dev.cel.common.types.CelType;
+import dev.cel.common.types.CelTypeProvider;
+import dev.cel.common.types.ListType;
+import dev.cel.common.types.MapType;
+import dev.cel.common.types.OptionalType;
+import dev.cel.common.types.SimpleType;
+import dev.cel.common.types.StructType;
+import dev.cel.common.types.StructTypeReference;
+import dev.cel.common.values.CelByteString;
+import dev.cel.common.values.CelValue;
+import dev.cel.common.values.CelValueConverter;
+import dev.cel.common.values.CelValueProvider;
+import dev.cel.common.values.StructValue;
+import dev.cel.compiler.CelCompilerLibrary;
+import dev.cel.runtime.CelRuntimeBuilder;
+import dev.cel.runtime.CelRuntimeLibrary;
+import java.lang.invoke.MethodHandle;
+import java.lang.invoke.MethodHandles;
+import java.lang.reflect.Constructor;
+import java.lang.reflect.Field;
+import java.lang.reflect.Method;
+import java.lang.reflect.Modifier;
+import java.lang.reflect.Type;
+import java.time.Duration;
+import java.time.Instant;
+import java.util.ArrayDeque;
+import java.util.Arrays;
+import java.util.HashSet;
+import java.util.List;
+import java.util.Map;
+import java.util.Objects;
+import java.util.Optional;
+import java.util.Queue;
+import java.util.Set;
+import java.util.function.BiConsumer;
+import java.util.function.Function;
+import org.jspecify.annotations.Nullable;
+
+/**
+ * Extension for supporting native Java types (POJOs) in CEL.
+ *
+ * This allows seamless plugin and evaluation of message creations and field selections without
+ * involving protobuf.
+ */
+@Immutable
+public final class CelNativeTypesExtensions implements CelCompilerLibrary, CelRuntimeLibrary {
+
+ private final NativeTypeRegistry registry;
+
+ // Set of all standard java.lang.Object method names.
+ private static final ImmutableSet OBJECT_METHOD_NAMES =
+ stream(Object.class.getDeclaredMethods()).map(Method::getName).collect(toImmutableSet());
+
+ private static final ImmutableMap, CelType> JAVA_TO_CEL_TYPE_MAP =
+ ImmutableMap., CelType>builder()
+ .put(boolean.class, SimpleType.BOOL)
+ .put(Boolean.class, SimpleType.BOOL)
+ .put(String.class, SimpleType.STRING)
+ .put(int.class, SimpleType.INT)
+ .put(Integer.class, SimpleType.INT)
+ .put(long.class, SimpleType.INT)
+ .put(Long.class, SimpleType.INT)
+ .put(UnsignedLong.class, SimpleType.UINT)
+ .put(float.class, SimpleType.DOUBLE)
+ .put(Float.class, SimpleType.DOUBLE)
+ .put(double.class, SimpleType.DOUBLE)
+ .put(Double.class, SimpleType.DOUBLE)
+ .put(byte[].class, SimpleType.BYTES)
+ .put(CelByteString.class, SimpleType.BYTES)
+ .put(Duration.class, SimpleType.DURATION)
+ .put(Instant.class, SimpleType.TIMESTAMP)
+ .put(Object.class, SimpleType.DYN)
+ .buildOrThrow();
+
+ private static final ImmutableMap, Object> JAVA_TO_DEFAULT_VALUE_MAP =
+ ImmutableMap., Object>builder()
+ .put(boolean.class, false)
+ .put(Boolean.class, false)
+ .put(String.class, "")
+ .put(int.class, 0L)
+ .put(Integer.class, 0L)
+ .put(long.class, 0L)
+ .put(Long.class, 0L)
+ .put(UnsignedLong.class, UnsignedLong.ZERO)
+ .put(float.class, 0.0)
+ .put(Float.class, 0.0)
+ .put(double.class, 0.0)
+ .put(Double.class, 0.0)
+ .put(byte[].class, new byte[0])
+ .put(CelByteString.class, CelByteString.EMPTY)
+ .put(Duration.class, Duration.ZERO)
+ .put(Instant.class, Instant.EPOCH)
+ .put(Optional.class, Optional.empty())
+ .buildOrThrow();
+
+ /** Creates a new instance of {@link CelNativeTypesExtensions} for the given classes. */
+ static CelNativeTypesExtensions nativeTypes(Class>... classes) {
+ return new CelNativeTypesExtensions(new NativeTypeRegistry(NativeTypeScanner.scan(classes)));
+ }
+
+ @VisibleForTesting
+ NativeTypeRegistry getRegistry() {
+ return registry;
+ }
+
+ @Override
+ public void setRuntimeOptions(CelRuntimeBuilder runtimeBuilder) {
+ runtimeBuilder.setValueProvider(registry);
+ runtimeBuilder.setTypeProvider(registry);
+ }
+
+ @Override
+ public void setCheckerOptions(CelCheckerBuilder checkerBuilder) {
+ checkerBuilder.setTypeProvider(registry);
+ }
+
+ /**
+ * NativeTypeScanner scans registered Java classes to extract properties and compile accessors.
+ */
+ @VisibleForTesting
+ static final class NativeTypeScanner {
+
+ private static final MethodHandles.Lookup LOOKUP = MethodHandles.lookup();
+
+ private NativeTypeScanner() {}
+
+ private static final class ScanResult {
+ private final ImmutableMap> classMap;
+ private final ImmutableMap typeMap;
+ private final ImmutableMap, StructType> classToTypeMap;
+ private final ImmutableMap, ImmutableMap> accessorMap;
+
+ ScanResult(
+ ImmutableMap> classMap,
+ ImmutableMap typeMap,
+ ImmutableMap, StructType> classToTypeMap,
+ ImmutableMap, ImmutableMap> accessorMap) {
+ this.classMap = classMap;
+ this.typeMap = typeMap;
+ this.classToTypeMap = classToTypeMap;
+ this.accessorMap = accessorMap;
+ }
+ }
+
+ private static ScanResult scan(Class>... classes) {
+ ImmutableMap.Builder> classMapBuilder = ImmutableMap.builder();
+ ImmutableMap.Builder typeMapBuilder = ImmutableMap.builder();
+ ImmutableMap.Builder, StructType> classToTypeMapBuilder = ImmutableMap.builder();
+ ImmutableMap.Builder, ImmutableMap> accessorMapBuilder =
+ ImmutableMap.builder();
+
+ Set> visited = new HashSet<>();
+ Queue> queue = new ArrayDeque<>(Arrays.asList(classes));
+
+ while (!queue.isEmpty()) {
+ Class> clazz = queue.poll();
+ if (shouldSkip(clazz, visited)) {
+ continue;
+ }
+ visited.add(clazz);
+
+ String typeName = getCelTypeName(clazz);
+ classMapBuilder.put(typeName, clazz);
+
+ ImmutableMap accessors = scanProperties(clazz, queue);
+ accessorMapBuilder.put(clazz, accessors);
+ }
+
+ ImmutableMap> classMap = classMapBuilder.buildOrThrow();
+ ImmutableMap, ImmutableMap> accessorMap =
+ accessorMapBuilder.buildOrThrow();
+
+ for (Map.Entry> entry : classMap.entrySet()) {
+ String typeName = entry.getKey();
+ Class> clazz = entry.getValue();
+
+ StructType structType = createStructType(clazz, classMap, accessorMap);
+ typeMapBuilder.put(typeName, structType);
+ classToTypeMapBuilder.put(clazz, structType);
+ }
+
+ ScanResult result =
+ new ScanResult(
+ classMap,
+ typeMapBuilder.buildOrThrow(),
+ classToTypeMapBuilder.buildOrThrow(),
+ accessorMap);
+
+ validateRegisteredClasses(result.classToTypeMap, result.classMap, result.accessorMap);
+
+ return result;
+ }
+
+ private static void validateRegisteredClasses(
+ ImmutableMap, StructType> classToTypeMap,
+ ImmutableMap> classMap,
+ ImmutableMap, ImmutableMap> accessorMap) {
+ for (Class> clazz : classToTypeMap.keySet()) {
+ for (String prop : getProperties(clazz)) {
+ try {
+ getPropertyType(clazz, prop, classMap, accessorMap);
+ } catch (IllegalArgumentException e) {
+ throw new IllegalArgumentException(
+ "Unsupported type for property '" + prop + "' in class " + clazz.getName(), e);
+ }
+ }
+ }
+ }
+
+ private static boolean shouldSkip(Class> clazz, Set> visited) {
+ return clazz == null
+ || visited.contains(clazz)
+ || clazz.isInterface()
+ || isSupportedType(clazz);
+ }
+
+ private static boolean isSupportedType(Class> type) {
+ return JAVA_TO_CEL_TYPE_MAP.containsKey(type)
+ || type == Optional.class
+ || List.class.isAssignableFrom(type)
+ || Map.class.isAssignableFrom(type)
+ || type.isArray();
+ }
+
+ private static StructType createStructType(
+ Class> clazz,
+ ImmutableMap> classMap,
+ ImmutableMap, ImmutableMap> accessorMap) {
+ return StructType.create(
+ getCelTypeName(clazz),
+ getProperties(clazz),
+ fieldName -> Optional.of(getPropertyType(clazz, fieldName, classMap, accessorMap)));
+ }
+
+ private static CelType getPropertyType(
+ Class> clazz,
+ String propertyName,
+ ImmutableMap> classMap,
+ ImmutableMap, ImmutableMap> accessorMap) {
+ ImmutableMap accessors = accessorMap.get(clazz);
+ if (accessors != null) {
+ PropertyAccessor accessor = accessors.get(propertyName);
+ if (accessor != null) {
+ return mapJavaTypeToCelType(accessor.targetType, accessor.genericTargetType, classMap);
+ }
+ }
+ throw new IllegalArgumentException("No public field or getter for " + propertyName);
+ }
+
+ private static CelType mapJavaTypeToCelType(
+ Class> type, Type genericType, ImmutableMap> classMap) {
+
+ CelType celType = JAVA_TO_CEL_TYPE_MAP.get(type);
+ if (celType != null) {
+ return celType;
+ }
+
+ if (type.isInterface()
+ && !List.class.isAssignableFrom(type)
+ && !Map.class.isAssignableFrom(type)) {
+ throw new IllegalArgumentException("Unsupported interface type: " + type.getName());
+ }
+
+ TypeToken> token = TypeToken.of(genericType);
+
+ if (List.class.isAssignableFrom(type)) {
+ Type elementType = resolveGenericParameter(token, List.class, 0);
+ return ListType.create(
+ mapJavaTypeToCelType(ReflectionUtil.getRawType(elementType), elementType, classMap));
+ }
+
+ if (Map.class.isAssignableFrom(type)) {
+ Type keyType = resolveGenericParameter(token, Map.class, 0);
+ Type valueType = resolveGenericParameter(token, Map.class, 1);
+
+ CelType celKeyType =
+ mapJavaTypeToCelType(ReflectionUtil.getRawType(keyType), keyType, classMap);
+ if (celKeyType == SimpleType.DOUBLE) {
+ throw new IllegalArgumentException("Decimals are not allowed as map keys in CEL.");
+ }
+
+ return MapType.create(
+ celKeyType,
+ mapJavaTypeToCelType(ReflectionUtil.getRawType(valueType), valueType, classMap));
+ }
+
+ // Optional is a final class, so reference equality is equivalent to isAssignableFrom
+ // but slightly more performant than tree traversal.
+ if (type == Optional.class) {
+ Type optionalType = resolveGenericParameter(token, Optional.class, 0);
+ return OptionalType.create(
+ mapJavaTypeToCelType(ReflectionUtil.getRawType(optionalType), optionalType, classMap));
+ }
+
+ String typeName = getCelTypeName(type);
+ if (classMap.containsKey(typeName)) {
+ return StructTypeReference.create(typeName);
+ }
+
+ throw new IllegalArgumentException(
+ "Unsupported Java type for CEL mapping: " + type.getName());
+ }
+
+ private static ImmutableMap scanProperties(
+ Class> clazz, Queue> queue) {
+ ImmutableMap.Builder builtAccessors = ImmutableMap.builder();
+
+ for (String propName : getProperties(clazz)) {
+ buildPropertyAccessor(clazz, propName, queue)
+ .ifPresent(accessor -> builtAccessors.put(propName, accessor));
+ }
+
+ return builtAccessors.buildOrThrow();
+ }
+
+ private static Optional buildPropertyAccessor(
+ Class> clazz, String propName, Queue> queue) {
+ Method getter = findGetter(clazz, propName);
+ Field field = findField(clazz, propName);
+
+ Class> propType = null;
+ Type genericPropType = null;
+ Function