A modern, lightweight, high-performance parser for the Common Expression Language (CEL) in Java.
dot-cel parses CEL expressions directly into a type-safe Java AST (represented by CelExpr records and sealed interfaces), and provides seamless serialization to standard CEL Protobuf messages.
Consider using dot-cel if your project has the following requirements:
Standard cel-java relies on the ANTLR runtime. In large enterprise deployments, different transitive dependencies using conflicting ANTLR versions can lead to runtime class-loading or linkage failures.
dot-cel has a zero-dependency footprint (built on top of dot-parse, Mug's lightweight parser combinator library). It eliminates ANTLR versioning conflicts entirely, making it highly suitable for shared libraries or framework integrations.
The AST in dot-cel is represented using Java sealed interfaces and records (CelExpr). This allows developers to write clean, type-safe, and exhaustive AST traversals or rewriters using Java 21 pattern matching:
String cel = ...;
switch (CelExpr.of(cel)) {
case FunctionCall(Ident(var name, _), List.of(var arg), _) when name.equals("size") -> ...
case MemberCall(var target, Ident(var member, _), var args, _) -> ...
default -> ...
}It's a significant ergonomic improvement over the traditional nested checks or AutoValue-based structures in cel-java.
Standard CEL parsers (including cel-java) immediately expand macro calls (like has(...) or .filter(...)) into complex, low-level AST representations (such as Comprehension or Select with testOnly=true). The original macro structure is lost in the parsed AST and only stored as metadata in SourceInfo.
dot-cel retains macros as first-class, semantically rich nodes in the AST (CelExpr.Macro.Filter, CelExpr.Macro.Exists, CelExpr.Macro.Has, etc.). This makes inspecting, traversing, or rewriting macros simple and type-safe (e.g. without having to parse complex 7-field Comprehension trees).
The standard low-level expanded AST protos are generated transparently by CelParser.parseToProto(), allowing seamless compatibility with the rest of the cel-java ecosystem (such as the type checker and interpreter).
dot-cel is optimized for parsing speed. JMH benchmarks show it parses expressions 1.5x to 2.7x faster than the standard ANTLR-based cel-java parser for expressions without comments, and runs on par for expressions with comments.
Unlike ANTLR-based parsers which maintain mutable state during parsing and force cel-java to allocate a fresh pipeline of stateful helper objects (Lexer, Parser, TokenStream, etc.) for every single parse invocation, combinator-based parsers are stateless and inherently thread-safe. A single static parser tree is reused globally, eliminating thread allocation overhead on the hot path.
Parser combinators are traditionally susceptible to JVM-crashing StackOverflowError when parsing deeply nested structures (e.g. (((...))) or long chained operations), making them risky for parsing untrusted user inputs.
dot-cel takes advantage of dot-parse's built-in stack depth protection. It safely limits recursion depth and throws a catchable ParseException instead of crashing the JVM, matching the production safety guarantees provided by cel-java.
dot-cel is a parser-only library. If you need type-checking or evaluation, you can easily bridge dot-cel to the cel-java runtime.
cel-java natively supports the standard com.google.api.expr.v1alpha1.ParsedExpr proto object produced by dot-cel:
CelParser parser = new CelParser();
// 1. Parse using dot-cel
com.google.api.expr.v1alpha1.ParsedExpr parsedExpr = parser.parseToProto("a[3]");
// 2. Wrap with CelProtoAbstractSyntaxTree directly
CelAbstractSyntaxTree ast = CelProtoAbstractSyntaxTree.fromParsedExpr(parsedExpr).getAst();
// 3. Perform type-checking or execution using cel-java
CelValidationResult validationResult = celCompiler.check(ast);If your classpath uses the newer dev.cel.expr package namespace, you can bridge the identical schemas via standard protobuf serialization/deserialization:
com.google.api.expr.v1alpha1.ParsedExpr parsedExpr = parser.parseToProto("a[3]");
// Translate namespaces via byte copy
dev.cel.expr.ParsedExpr celJavaExpr =
dev.cel.expr.ParsedExpr.parseFrom(parsedExpr.toByteArray());
CelAbstractSyntaxTree ast = CelProtoAbstractSyntaxTree.fromParsedExpr(celJavaExpr).getAst();