Add an EnvOption to allow undeclared variables to default to cel.DynType during Compile
Background
In some dynamic use-cases, CEL expressions may reference variables that are not known at environment construction time. Currently, env.Compile fails with an error when encountering undeclared identifiers.
While PartialActivation, UnknownAttributePattern, and EvalOptions help during evaluation, there is no option that allows Compile to accept undeclared variables by treating them as cel.DynType.
Avoiding Workarounds Using ctx or data Prefix
A common workaround today is: Wrap all variables under a single declared object such as ctx.<varName> or data.<varName>, and Pass the actual values inside that object during evaluation.
However, this approach has drawbacks: Requires rewriting user expressions (a + b.c → ctx.a + ctx.b.c). Leaks implementation details into the expression language. Breaks ergonomics and readability of the CEL expression. Forces a global top-level namespace that may conflict with future declared variables or builtins.
By the way, would the resulting behavior be equivalent if I only perform Parse without running Check?
Best regards,
Acynothia
Hi @acynothia, is the type actually know for the variable, but not the value? You can create expressions which support unknown top-level variables, but you'd want to know the types if possible. Generally, it's a bad idea to accept undefined variables into the expression ... a parse-only approach will skip any type check validation, but it definitely exposes you to more risk w.r.t. an errant expression at runtime.
Thank you very much for the response.
In some rule-engine scenarios, both the user inputs and the CEL expressions are dynamically configured. It is difficult to know all variable names and their types in advance at initialization time. Our current workaround is to wrap all inputs into a top-level data object (a map[string]any). This allows expressions to consistently reference variables as data.<var_name>, and we only need to declare the data object itself. However, this approach comes with the drawbacks mentioned above.
That’s why I’m wondering whether it might be possible to fall back to cel.DynType when an identifier is not found during the Ident check—effectively bypassing strict type checking. Personally, many scripting languages support dynamic or weak typing, and type mismatches at runtime are still common in such systems. Even with CEL’s static type checking, if user input contains map[string]any values, runtime errors may still occur when actual types do not match request ones. This seems to be a common situation in most scripting-language-like environments.
Therefore, I think it might be beneficial to provide an Option that allows users to decide whether type checking should be enforced. Conceptually, such an option would behave similarly to wrapping everything under a global data object of type cel.DynType, but without the side effects and ergonomics issues introduced by forcing all variables into a single global namespace.
Hi @acynothia,
While I can appreciate what you're aiming to do, the scenario you described is less than ideal since it means that you could have cycles or collisions at some point in the rule graph unless you add a lot of additional code checks. Generally, having the variables as well-typed helps with ensuring declaration before use, and validation of a cycle-free rule graph.
Given that this would be a pretty significant change that could cause problems in production, I'd recommend considering a different approach. You can, of course, always Parse and then Check later if you need to sort out dependencies, but you might find that you have a fair amount of code to maintain.
I know the answer may be disappointing, but I do hope the discussion was useful and you're able to find a way to meet your needs.
Regards,
-Tristan
Thanks for the explanation. I plan to experiment with building a helper function based on the AST that automatically generates declarations, declaring any identifiers that appear in the expression as cel.DynType.