Skip to content

Strings and numbers

CEL provides built-in functions for working with strings and standard operators for comparing numbers. For single-field checks, always prefer standard rules — they’re more concise and optimized. These CEL functions become essential when you need cross-field validation, conditional logic, or combinations that standard rules can’t express on their own.

The size() function returns the number of characters in a string. Use it to enforce length requirements on string fields.

Check that a string is non-empty:

size(this) > 0

Check that a string is within a length range:

size(this) >= 3 && size(this) <= 50

For simple length checks, use standard rules like min_len and max_len. size() in CEL is useful when the check depends on another field or a condition. In this example, a custom reason is required only when the ticket category is "other" — something standard rules can’t express:

import "buf/validate/validate.proto";
message SupportTicket {
string category = 1;
string custom_reason = 2;
// A custom reason is required for "other" category tickets.
option (buf.validate.message).cel = {
id: "ticket.custom_reason_required"
message: "custom_reason is required for 'other' category"
expression: "this.category != 'other' || size(this.custom_reason) > 0"
};
}

Substrings: contains(), startsWith(), endsWith()

Section titled “Substrings: contains(), startsWith(), endsWith()”

CEL provides methods for checking whether a string includes, begins with, or ends with a given substring.

Check whether a string contains a substring:

this.contains("@")

Check whether a string starts with a prefix:

this.startsWith("https://")

Check whether a string ends with a suffix:

this.endsWith(".com")

Standard rules like prefix and suffix handle single-field substring checks. These CEL methods are useful when you need to compare strings across fields. In this example, the callback URL must be under the configured allowed origin — a cross-field check that standard rules can’t express:

import "buf/validate/validate.proto";
message WebhookConfig {
string allowed_origin = 1;
string callback_url = 2;
// The callback URL must be under the allowed origin.
option (buf.validate.message).cel = {
id: "webhook.callback_origin"
message: "callback URL must start with the allowed origin"
expression: "this.callback_url.startsWith(this.allowed_origin)"
};
}

The matches() function tests a string against a RE2 regular expression. Use it when substring checks aren’t specific enough and you need full pattern matching.

Match a simple alphanumeric pattern:

this.matches("^[a-zA-Z0-9]+$")

Match a semantic version string like 1.2.3:

this.matches("^[0-9]+\\.[0-9]+\\.[0-9]+$")

Match a slug-style identifier (lowercase letters, digits, and hyphens):

this.matches("^[a-z0-9]+(-[a-z0-9]+)*$")

The standard rule pattern handles static regex checks on a single field. matches() in CEL is useful when the expected format depends on another field’s value. In this example, the tax_id must match a format that varies by country — something a single standard rule can’t express:

import "buf/validate/validate.proto";
message BusinessRegistration {
string country = 1;
string tax_id = 2;
// Tax ID format depends on the country.
option (buf.validate.message).cel = {
id: "registration.tax_id_format"
message: "tax ID does not match the expected format for this country"
expression:
"(this.country != 'US' || this.tax_id.matches('^[0-9]{2}-[0-9]{7}$'))"
"&& (this.country != 'GB' || this.tax_id.matches('^[0-9]{9,10}$'))"
};
}

CEL supports the standard comparison operators for numbers: >, >=, <, <=, ==, and !=. Combine them with && to validate that a value falls within a range.

Basic comparisons:

this > 0
this >= 1 && this <= 100
this != 0

When comparing against literal values, use the correct type suffix to match the Protobuf field’s CEL type. Protobuf uint32 and uint64 fields become CEL uint, which requires the u suffix on literals. Protobuf float and double fields become CEL double, which requires a decimal point:

this > 0u // for uint32 or uint64 fields
this >= 1.0 // for float or double fields
this > 0 && this < 1000 // for int32 or int64 fields

Standard rules like gt, gte, lt, and lte handle single-field range checks. CEL numeric operators are essential for cross-field comparisons. In this example, min_bedrooms and max_bedrooms must form a valid range — note the u suffix since these are uint32 fields:

import "buf/validate/validate.proto";
message SearchFilters {
uint32 min_bedrooms = 1;
uint32 max_bedrooms = 2;
// Minimum bedrooms must not exceed maximum.
option (buf.validate.message).cel = {
id: "search.bedroom_range"
message: "minimum bedrooms must not exceed maximum"
expression: "this.min_bedrooms <= this.max_bedrooms"
};
}

Use && (logical AND) and || (logical OR) to combine string and numeric checks into a single expression. For example, you might validate that a discount code is non-empty only when a discount percentage is greater than zero:

this.discount_percentage == 0 || size(this.discount_code) > 0

For more on combining expressions with logical operators, ternary conditions, and short-circuit evaluation, see Logic and conditions.