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.
String length with size()
Section titled “String length with size()”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) > 0Check that a string is within a length range:
size(this) >= 3 && size(this) <= 50For 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)" };}Pattern matching with matches()
Section titled “Pattern matching with matches()”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}$'))" };}Numeric comparisons
Section titled “Numeric comparisons”CEL supports the standard comparison operators for numbers: >, >=, <, <=, ==, and !=. Combine them with && to validate that a value falls within a range.
Basic comparisons:
this > 0this >= 1 && this <= 100this != 0When 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 fieldsthis >= 1.0 // for float or double fieldsthis > 0 && this < 1000 // for int32 or int64 fieldsStandard 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" };}Combining string and numeric checks
Section titled “Combining string and numeric checks”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) > 0For more on combining expressions with logical operators, ternary conditions, and short-circuit evaluation, see Logic and conditions.
Learn more
Section titled “Learn more”- Strings and numbers on CEL by Example — String and number expressions in plain CEL.
- Strings and bytes type reference on CEL by Example — Detailed reference for CEL string and bytes types.
- Booleans and numbers type reference on CEL by Example — Detailed reference for CEL boolean and number types.
- CEL extensions — Protovalidate’s CEL extensions including
isEmail(),isHostname(), andisUri(). - Custom CEL rules — Full guide to writing custom CEL validation rules.