This is a Java port of the Common-Expression-Language (CEL). The CEL specification can be found here.
The easiest way to get started is to add the CEL-Java BOM, cel-tools, and one generated protobuf
artifact to your project.
Maven:
<dependencyManagement>
<dependencies>
<dependency>
<groupId>org.projectnessie.cel</groupId>
<artifactId>cel-bom</artifactId>
<version>0.6.2</version>
<type>pom</type>
<scope>import</scope>
</dependency>
</dependencies>
</dependencyManagement>
<dependencies>
<dependency>
<groupId>org.projectnessie.cel</groupId>
<artifactId>cel-generated-pb</artifactId>
</dependency>
<dependency>
<groupId>org.projectnessie.cel</groupId>
<artifactId>cel-tools</artifactId>
</dependency>
</dependencies>Gradle:
dependencies {
implementation(enforcedPlatform("org.projectnessie.cel:cel-bom:0.6.2"))
implementation("org.projectnessie.cel:cel-generated-pb")
implementation("org.projectnessie.cel:cel-tools")
}The cel-bom artifact is available for CEL-Java version 0.3.0 and newer.
Starting with CEL-Java 0.7.0, cel-core, cel-tools, and the Jackson integration artifacts no
longer choose a generated protobuf runtime transitively. Add exactly one of these artifacts:
org.projectnessie.cel:cel-generated-pborg.projectnessie.cel:cel-generated-pb3
Do not put both generated protobuf artifacts on the same classpath; they provide the same generated CEL protobuf classes for different protobuf runtime choices.
The cel-tools artifact provides ScriptHost as a simple entry point for producing reusable
Script instances.
import java.util.HashMap;
import java.util.Map;
import org.projectnessie.cel.checker.Decls;
import org.projectnessie.cel.tools.Script;
import org.projectnessie.cel.tools.ScriptHost;
public class MyClass {
public void myScriptUsage() {
ScriptHost scriptHost = ScriptHost.newBuilder().build();
Script script = scriptHost.buildScript("x + ' ' + y")
.withDeclarations(
Decls.newVar("x", Decls.String),
Decls.newVar("y", Decls.String))
.build();
Map<String, Object> arguments = new HashMap<>();
arguments.put("x", "hello");
arguments.put("y", "world");
String result = script.execute(String.class, arguments);
System.out.println(result); // Prints "hello world"
}
}Protobuf objects and schemas are supported through the generated protobuf artifact selected in your
dependencies, either cel-generated-pb or cel-generated-pb3.
syntax = "proto3";
message MyPojo {
string Property1 = 1;
}public class MyClass {
public Boolean evalWithProtobuf() {
ScriptHost scriptHost = ScriptHost.newBuilder().build();
Script script =
scriptHost
.buildScript("inp.Property1 == checkName")
.withDeclarations(
// protobuf types need the type's full name
Decls.newVar("inp", Decls.newObjectType(MyPojo.getDescriptor().getFullName())),
Decls.newVar("checkName", Decls.String))
// protobuf types need the default instance
.withTypes(MyPojo.getDefaultInstance())
.build();
MyPojo pojo = MyPojo.newBuilder().setProperty1("test").build();
Map<String, Object> arguments = new HashMap<>();
arguments.put("inp", pojo);
arguments.put("checkName", "test");
return script.execute(Boolean.class, arguments);
}
}Plain Java objects can also be exposed through Jackson's bean/property model by using the
org.projectnessie.cel.types.jackson3.Jackson3Registry from org.projectnessie.cel:cel-jackson3.
Use this registry when the object is not a protobuf message and CEL-Java should read properties the
same way Jackson would serialize them, including JavaBean getters, records, fields, and Jackson
annotations such as @JsonProperty.
import org.projectnessie.cel.types.jackson3.Jackson3Registry;
public class MyClass {
public Boolean evalWithJacksonObject(MyInput input, String checkName) {
ScriptHost scriptHost = ScriptHost.newBuilder()
.registry(Jackson3Registry.newRegistry())
.build();
Script script = scriptHost.buildScript("inp.name == checkName")
.withDeclarations(
// types for Jackson need the fully qualified class name
Decls.newVar("inp", Decls.newObjectType(MyInput.class.getName())),
Decls.newVar("checkName", Decls.String))
// Register the Jackson object input type as a java.lang.Class.
.withTypes(MyInput.class)
.build();
Map<String, Object> arguments = new HashMap<>();
arguments.put("inp", input);
arguments.put("checkName", checkName);
return script.execute(Boolean.class, arguments);
}
}Jackson field names are used as CEL-Java property names. It is not necessary to annotate plain Java classes with Jackson annotations.
To use Jackson 3, add cel-jackson3 in addition to cel-tools or cel-core:
dependencies {
implementation(enforcedPlatform("org.projectnessie.cel:cel-bom:0.6.2"))
implementation("org.projectnessie.cel:cel-generated-pb")
implementation("org.projectnessie.cel:cel-tools")
implementation("org.projectnessie.cel:cel-jackson3")
}Jackson 2 support is similar:
- Use
JacksonRegistryfromorg.projectnessie.cel.types.jackson.JacksonRegistry. - Use
org.projectnessie.cel:cel-jacksoninstead oforg.projectnessie.cel:cel-jackson3.
CEL-Java can be embedded behind an application-owned authorization decision point. CEL evaluates the expression, but the application remains responsible for defining available attributes, principal and role semantics, resource inheritance, and the final fail-closed decision.
An authorization expression can be compiled once and evaluated many times with different arguments:
import java.time.Instant;
import java.util.HashMap;
import java.util.Map;
import org.projectnessie.cel.checker.Decls;
import org.projectnessie.cel.tools.Script;
import org.projectnessie.cel.tools.ScriptException;
import org.projectnessie.cel.tools.ScriptHost;
public class AuthorizationExample {
private static final String EXPRESSION =
"resource.service == \"storage.googleapis.com\""
+ " && resource.type == \"storage.googleapis.com/Object\""
+ " && resource.name.startsWith(\"projects/_/buckets/example/objects/reports/\")"
+ " && request.time < timestamp(\"2026-08-01T00:00:00Z\")";
private final Script condition;
public AuthorizationExample() throws ScriptException {
condition =
ScriptHost.newBuilder()
.build()
.buildScript(EXPRESSION)
.withDeclarations(
Decls.newVar("resource.service", Decls.String),
Decls.newVar("resource.type", Decls.String),
Decls.newVar("resource.name", Decls.String),
Decls.newVar("request.time", Decls.Timestamp))
.build();
}
public boolean grants(String service, String type, String name, Instant requestTime) {
Map<String, Object> arguments = new HashMap<>();
arguments.put("resource.service", service);
arguments.put("resource.type", type);
arguments.put("resource.name", name);
arguments.put("request.time", requestTime);
try {
return Boolean.TRUE.equals(condition.execute(Boolean.class, arguments));
} catch (ScriptException | RuntimeException e) {
return false;
}
}
}For authorization uses, parse/check failures while building the Script, runtime errors while
evaluating it, unknown results, and non-boolean results should be treated as non-granting unless the
host application intentionally defines different behavior. The broad RuntimeException catch above
is intentional at the authorization boundary because native conversion failures and unexpected
evaluation failures should not grant access.
Custom functions can be added by implementing the
org.projectnessie.cel.Library
interface. The interface provides declarations via List<EnvOption> getCompileOptions() and
runtime implementations via List<ProgramOption> getProgramOptions().
Examples are:
StdLibraryStringsLibMyLibinScriptHostTest- CEL-Go examples for encoders and strings
Receiver-style functions are declared with Decls.newInstanceOverload(...); the receiver value is
passed to the runtime function as the first argument. This is the right place for application-specific
helpers such as resource-name parsing. Such helpers are host extensions, not portable CEL standard
functions.
ScriptHost currently builds scripts with CEL's standard library. Hosts that need stricter function
or macro subsets can use the lower-level Env.newCustomEnv(...) API; ScriptHost does not
currently expose a no-standard-library construction option.
| Need | Use |
|---|---|
Normal embedding with ScriptHost |
cel-tools plus exactly one of cel-generated-pb or cel-generated-pb3 |
| Dependency isolation / relocated protobuf dependencies | cel-standalone |
| Jackson 3 object access | cel-tools or cel-core plus cel-jackson3 and one generated protobuf artifact |
| Jackson 2 object access | cel-tools or cel-core plus cel-jackson and one generated protobuf artifact |
Use either cel-tools or cel-standalone, never both.
The org.projectnessie.cel:cel-standalone artifact contains CEL-Java's core runtime and relocated
protobuf dependencies in one dependency-isolated artifact.
Using cel-standalone is especially useful when your project requires different versions of
protobuf-java.
Jackson runtimes are intentionally not relocated into cel-standalone. If you need CEL-Java's
Jackson functionality, choose the Jackson version you want and include those Jackson dependencies in
your project.
The Common Expression Language allows simple computations against data structures.
Project Nessie aims to use CEL to enforce security policies and for various filtering expressions.
This Java implementation of CEL is based on the CEL-Go implementation.
Typed data structures should be defined using protobuf, but arbitrary data structures using Java wrapper data types, lists, and maps work too.
For example, this expression from the
CEL-Go codelab exercise7
checks whether the extra_claims map of a JWT contains an entry with a key starting with group
and a value ending with @acme.co:
jwt.extra_claims.exists(c, c.startsWith('group'))
&& jwt.extra_claims.filter(c, c.startsWith('group'))
.all(c, jwt.extra_claims[c]
.all(g, g.endsWith('@acme.co')))The JWT argument can be represented as a map:
import java.util.List;
import java.util.Map;
Map<String, Object> jwt = Map.of(
"jwt", Map.of(
"sub", "serviceAccount:delegate@acme.co",
"aud", "my-project",
"iss", "auth.acme.com:12350",
"extra_claims", Map.of(
"group1", List.of("admin@acme.co", "analyst@acme.co"),
"labels", List.of("metadata", "prod", "pii"),
"groupN", List.of("forever@acme.co")
)
)
);CEL-Java does not support access to arbitrary Java classes. This means you cannot access standard Java functionality from a CEL expression, nor is it intended or planned to do so.
CEL is intentionally non-Turing-complete: it ends in a finite amount of time and has no loops or other blocking operations.
Use custom functions to provide application-owned functionality to CEL scripts.
The CEL type system has two
64-bit integer types: signed int and unsigned uint. Objects and fields of different types must
be explicitly cast in CEL. The Java wrapper type for CEL-Java's unsigned uint is
org.projectnessie.cel.common.ULong.
If you do not explicitly define a uint type or indirectly use uint via protobuf, you will
probably never notice it.
Java does not have a native primitive uint32 or uint64. To maintain conformance to the CEL spec,
CEL-Java treats CEL's uint type differently from int. For example, 123 == 123u is not true,
but 123u == 123u and 123 == 123 are.
If you have a uint32 or uint64 in protobuf objects, or use uints in CEL expressions, wrap those
values with org.projectnessie.cel.common.ULong.
Native-image and package behavior must be verified in the consuming application's exact build.
cel-standalone can reduce dependency conflicts by relocating protobuf dependencies, but it does not
prove Quarkus native-image or package compatibility for every application.
Before using CEL conditions in release-critical authorization paths, run JVM condition tests, the consuming project's normal build, dependency tree review for protobuf/ANTLR/Jackson conflicts, and package/native-image verification if native execution is part of the release path.
- JSON extension (see spec
and for example
nonFiniteincom_github_golang_protobuf/jsonpb/decode.go, around line 441) - Encoders extension (like in Go), not difficult to port to Java, but work to be done at some point.
Rounding/truncating of numeric values, especially when converting the CEL type double to int or
uint, is ambiguous. The CEL spec says: CEL provides no way to control the finer points of
floating-point arithmetic, such as expression evaluation, rounding mode, or exception handling.
However, any two not-a-number values will compare equal even if their underlying properties are
different. (see spec).
The CEL-Go unit test common/types/double_test.go/TestDoubleConvertToType asserts on -5 for the
CEL expression int(-4.5), because CEL-Go uses math.Round(float64).
Since the CEL spec is not clear, and the CEL conformance tests assert on double-to-int truncation
(Java-like: double doubleValue; long res = (long) doubleValue;), CEL-Java implements the behavior
that passes the CEL-spec conformance tests.
Go's math.Round(float64) behaves differently from Java's Math.round(double) or Math.rint(),
so a 1:1 port of the CEL-Go behavior is not trivial.
The CEL-Go implementation does not pass these CEL-spec conformance tests:
--- FAIL: TestSimpleFile/conversions/int/double_truncate (0.01s)
simple_test.go:219: double_truncate: Eval got [int64_value:2], want [int64_value:1]
--- FAIL: TestSimpleFile/conversions/int/double_truncate_neg (0.01s)
simple_test.go:219: double_truncate_neg: Eval got [int64_value:-8], want [int64_value:-7]
--- FAIL: TestSimpleFile/conversions/int/double_half_pos (0.01s)
simple_test.go:219: double_half_pos: Eval got [int64_value:12], want [int64_value:11]
--- FAIL: TestSimpleFile/conversions/int/double_half_neg (0.01s)
simple_test.go:219: double_half_neg: Eval got [int64_value:-4], want [int64_value:-3]
--- FAIL: TestSimpleFile/conversions/uint/double_truncate (0.01s)
simple_test.go:219: double_truncate: Eval got [uint64_value:2], want [uint64_value:1]
--- FAIL: TestSimpleFile/conversions/uint/double_half (0.01s)
simple_test.go:219: double_half: Eval got [uint64_value:26], want [uint64_value:25]
The CEL-Java repo uses git submodules to pull in required APIs from Google and the CEL spec. Those submodules are required to build the CEL-Java project.
Run git submodule init and git submodule update after a fresh clone.
Build requirements:
- Java 21 or newer
- Gradle wrapper from this repository
Runtime requirements:
- Java 8 or newer
./gradlew publishToMavenLocal deploys the current development version to the local Maven
repository, in case you want to use CEL-Java snapshot artifacts from another project.
./gradlew test builds the production code and runs the unit tests.
The project uses Google Java style and the Spotless plugin. Run ./gradlew spotlessApply to fix
formatting issues.
To run the CEL-spec conformance tests, Go, Bazel, and their toolchains are required. From the
CEL-Java repo, run conformance/run-conformance-tests.sh. That script performs the necessary Gradle
and Bazel builds.