CEL overview
Common Expression Language (CEL) is a small expression language that Protovalidate uses for all of its validation rules. Every standard rule is a CEL expression under the hood, and when standard rules aren’t enough, you write custom rules directly in CEL.
This page covers the CEL you need to know to write Protovalidate validation rules. If you’re already comfortable with CEL and want to jump straight into writing rules, see custom rules. For an interactive way to learn CEL itself, see CEL by Example.
Expressions and this
Section titled “Expressions and this”A CEL expression is a single line of code that evaluates to a value. In Protovalidate, expressions evaluate to true (validation passes) or false (validation fails).
Every Protovalidate CEL expression has access to a special variable called this. In a field rule, this is the field’s value. In a message rule, this is the message itself, and you access its fields with dot notation.
// In a field rule, this is the field's value.size(this) >= 3
// In a message rule, this is the message. Access fields with dot notation.this.start < this.endHere’s what that looks like in a Protovalidate schema. The fields use standard rules for simple single-field checks. The message has a CEL rule where this is the entire CreateUserRequest, letting you compare password and confirm_password — something standard rules can’t express:
import "buf/validate/validate.proto";
message CreateUserRequest { // Standard rules handle simple single-field checks. string username = 1 [(buf.validate.field).string.min_len = 3]; string password = 2 [(buf.validate.field).string.min_len = 8]; string confirm_password = 3;
// CEL handles cross-field validation that standard rules can't express. option (buf.validate.message).cel = { id: "password.confirmation" message: "password and confirmation must match" expression: "this.password == this.confirm_password" };}Protobuf types are converted to CEL types when your expression runs. When comparing this against a literal value, you must use the correct CEL literal syntax. The most common mistake is writing 1 (a CEL int) when the Protobuf field is uint32 or uint64, which requires 1u.
| Protobuf type | CEL type | Literal examples |
|---|---|---|
bool | bool | true, false |
int32, int64, sint32, sint64 | int | 1, -42, 0 |
uint32, uint64, fixed32, fixed64 | uint | 1u, 0u, 100u |
float, double | double | 1.0, -3.14, 0.0 |
string | string | "hello", 'world' |
bytes | bytes | b"hello", b'\x00' |
enum | int | 0, 1, 2 |
repeated T | list | [1, 2, 3] |
map<K, V> | map | {"key": "value"} |
google.protobuf.Duration | duration | duration("1h30m") |
google.protobuf.Timestamp | timestamp | timestamp("2024-01-01T00:00:00Z") |
Wrapper types (StringValue, etc.) | nullable | null when unset |
If needed, you can convert between types with CEL’s built-in conversion functions: int(x), uint(x), double(x), string(x).
For the complete mapping, see Protocol Buffer Data Conversion in the CEL language specification.
Operators
Section titled “Operators”CEL supports the standard comparison and logical operators. If you’ve used any C-style language, these are familiar:
| Operator | Meaning | Example |
|---|---|---|
==, != | Equality | this == "active" |
<, <=, >, >= | Comparison | this >= 1u && this <= 100u |
&& | Logical AND | size(this) >= 3 && size(this) <= 50 |
|| | Logical OR | this == "admin" || this == "editor" |
! | Logical NOT | !this.contains("test") |
+ | Addition, string concat, list concat | this.first + " " + this.last |
in | List/map membership | this in ["USD", "EUR", "GBP"] |
? : | Conditional (ternary) | this > 0 ? "" : "must be positive" |
&& and || use short-circuit evaluation: if the left side determines the result, the right side is never evaluated. This isn’t just an optimization — it’s how you write guard conditions that prevent runtime errors like division by zero or index out of bounds. See logic and conditions and common errors for details.
Functions
Section titled “Functions”CEL functions can be called as methods on a value or as standalone functions. Both styles are equivalent:
// Method style.this.startsWith("https://")this.contains("@")"hello".size()
// Function style.size(this)has(this.nickname)These are the most common functions you’ll use in Protovalidate rules:
| Function | What it does | Example |
|---|---|---|
size() | Length of a string, bytes, list, or map | size(this) > 0 |
contains() | Whether a string contains a substring | this.contains("@") |
startsWith() | Whether a string starts with a prefix | this.startsWith("https://") |
endsWith() | Whether a string ends with a suffix | this.endsWith(".com") |
matches() | Whether a string matches an RE2 regex | this.matches("^[a-z]+$") |
has() | Whether a field is set on a message | has(this.nickname) |
all() | Whether every list element satisfies a condition | this.all(x, x > 0) |
exists() | Whether at least one list element matches | this.exists(x, x > 100) |
filter() | Select list elements matching a condition | this.filter(x, x > 0) |
Protovalidate adds its own extension functions for common validation tasks: isEmail(), isHostname(), isUri(), isIp(), unique(), and more. See the extensions reference for the full list.
Dynamic error messages
Section titled “Dynamic error messages”By default, a CEL expression returns true or false, and you provide a static error message in the message field. But if you omit message and write an expression that returns a string instead, Protovalidate uses that string as the error message. An empty string means validation passed; a non-empty string is the failure message. This lets you include runtime values:
import "buf/validate/validate.proto";
message TransferRequest { // Standard rules handle simple single-field checks. uint64 amount = 1 [(buf.validate.field).uint64.gt = 0]; uint64 balance = 2;
// CEL produces a dynamic error message with runtime values. option (buf.validate.message).cel = { id: "transfer.sufficient_balance" expression: "this.amount <= this.balance ? ''" ": 'cannot transfer ' + string(this.amount)" " + ' with a balance of ' + string(this.balance)" };}Next steps
Section titled “Next steps”Now that you understand CEL basics, explore specific topics:
- Strings and numbers — String functions, pattern matching, and numeric comparisons.
- Collections and macros — Repeated fields, maps, and macros like
all(),exists(), andfilter(). - Logic and conditions — Logical operators, the ternary operator, and short-circuit evaluation.
- Common errors — Runtime pitfalls like division by zero, null wrappers, and guard patterns.
- Custom rules — How to add CEL validation rules to your Protobuf schemas.
- Extensions reference — Protovalidate-specific CEL variables and functions.
- CEL by Example — Interactive CEL tutorials at celbyexample.com.
To understand how CEL works under the hood and why Protovalidate chose it, see how Protovalidate uses CEL.