Logic and conditions
CEL provides logical operators and conditional expressions that let you combine multiple checks into a single validation rule. These are essential for writing Protovalidate rules that go beyond simple, single-field constraints.
Logical operators
Section titled “Logical operators”AND (&&)
Section titled “AND (&&)”The && operator requires both conditions to be true. This is the most common way to combine checks in validation rules. CEL's && is essential for cross-field validation:
this.min_price >= 0.0 && this.min_price <= this.max_pricesize(this.name) > 0 && !this.name.contains(this.secret)OR (||)
Section titled “OR (||)”The || operator requires at least one condition to be true. Use it to require that at least one of several fields is present or to provide alternatives:
has(this.email) || has(this.phone)this.status == "active" || this.override_enabledNOT (!)
Section titled “NOT (!)”The ! operator negates a boolean expression.
!this.contains("password")!(this.startsWith("test_") && this.size() < 10)Grouping with parentheses
Section titled “Grouping with parentheses”Parentheses control evaluation order, just like in most programming languages. Without them, && binds more tightly than ||.
(this.startsWith("prod-") || this.startsWith("staging-")) && this.size() <= 63The conditional (ternary) operator
Section titled “The conditional (ternary) operator”CEL supports the ternary operator condition ? value_if_true : value_if_false for conditional logic. In Protovalidate, this is especially useful when a CEL expression returns a string instead of a bool: an empty string means the rule passes, and a non-empty string becomes the validation error message. This lets you produce dynamic error messages that include runtime values.
this.amount <= this.balance ? "" : "insufficient balance"Here's how that looks in a Protovalidate rule. The ternary produces a dynamic error message with actual field values — something a static message string can't do:
import "buf/validate/validate.proto";
message TransferRequest { uint64 amount = 1; uint64 balance = 2;
option (buf.validate.message).cel = { id: "transfer.sufficient_balance" // The expression returns a string: empty for success, non-empty for failure. // The returned string becomes the violation message. expression: "this.amount <= this.balance ? ''" ": 'cannot transfer ' + string(this.amount)" " + ' with a balance of ' + string(this.balance)" };}Short-circuit evaluation
Section titled “Short-circuit evaluation”CEL evaluates && and || left to right and stops as soon as the result is determined. With &&, if the left side is false, the right side is never evaluated. With ||, if the left side is true, the right side is skipped. This isn't just an optimization — it lets you write guard conditions that prevent runtime errors.
Guard before indexing
Section titled “Guard before indexing”Check that a list has elements before accessing the first one:
size(this) > 0 && this[0] == "primary"Without the guard, accessing this[0] on an empty list would cause an index-out-of-bounds error.
Guard before division
Section titled “Guard before division”Check for zero before dividing:
this.denominator != 0 && this.numerator / this.denominator < 100For more on runtime errors like these, see common errors.
Conditional field validation
Section titled “Conditional field validation”A common Protovalidate pattern for optional fields is !has(this.field) || condition. This reads as "if the field is set, it must satisfy the condition." If the field isn't set, the rule passes without evaluating the condition.
!has(this.nickname) || this.nickname.size() >= 2Here's a complete example using this pattern in a Protovalidate rule:
import "buf/validate/validate.proto";
message UserProfile { // name is always required. string name = 1 [(buf.validate.field).string.min_len = 1];
// nickname is optional, but if provided it must be between // 2 and 30 characters. optional string nickname = 2;
// The message-level rule validates nickname only when it's present. option (buf.validate.message).cel = { id: "nickname.length" message: "nickname must be between 2 and 30 characters" expression: "!has(this.nickname) || (this.nickname.size() >= 2 && this.nickname.size() <= 30)" };}Complete validation rules
Section titled “Complete validation rules”Real-world validation often combines several logical operators, field presence checks, and conditional expressions into a single rule. Here's an example that validates a shipping request with multiple interdependent constraints:
import "buf/validate/validate.proto";
message ShippingRequest { option (buf.validate.message).cel = { id: "shipping.express_weight_limit" // When the expression returns a string, an empty string means // validation passed and a non-empty string is the error message. expression: "!has(this.express) || !this.express ? ''" ": this.weight_kg <= 30.0 ? ''" ": 'express shipping is limited to 30 kg, but package weighs '" " + string(this.weight_kg) + ' kg'" };
string destination = 1 [(buf.validate.field).string.min_len = 1]; double weight_kg = 2 [(buf.validate.field).double.gt = 0.0]; bool express = 3;}Learn more
Section titled “Learn more”- Logic and conditions on CEL by Example: an interactive overview of combining CEL expressions.
- Logical operators on CEL by Example: details on
&&,||, and!. - Conditional operator on CEL by Example: the ternary operator in depth.
- Custom CEL rules: how to write your own validation rules with CEL in Protovalidate.