Collections and macros
CEL provides a set of macros and operators for working with lists and maps. These are especially useful in Protovalidate rules that validate repeated fields, map fields, or relationships between collection elements.
This page covers the most common collection operations and how to use them in Protovalidate rules. For a full guide to writing CEL-based rules, see custom rules.
Membership and size
Section titled “Membership and size”The in operator checks whether a value exists in a list. The size() function returns the number of elements in a list or map, or the number of characters in a string.
// Check if a value is in a list."apple" in ["apple", "banana", "cherry"] // true
// Get the size of a list.[1, 2, 3].size() // 3For static membership checks, use the standard rule in. For simple list length, use min_items and max_items. The in operator and size() in CEL are useful for cross-field checks that standard rules can't express. In this example, the primary contact must be one of the team members:
import "buf/validate/validate.proto";
message ProjectRequest { string primary_contact = 1; repeated string team_members = 2;
// The primary contact must be one of the team members. option (buf.validate.message).cel = { id: "project.contact_in_team" message: "primary contact must be a team member" expression: "this.primary_contact in this.team_members" };}all() macro
Section titled “all() macro”The all() macro returns true when every element in a list satisfies a predicate. It returns true for empty lists (vacuous truth), so if the list must be non-empty, combine all() with a size() check.
// Every number is positive.[1, 2, 3].all(n, n > 0) // true
// An empty list satisfies any condition.[].all(n, n > 0) // trueIn a Protovalidate rule, all() can validate properties across every element in a repeated field:
import "buf/validate/validate.proto";
message InviteUsersRequest { // Every email address must contain an '@' symbol. repeated string emails = 1 [(buf.validate.field).cel = { id: "invite.valid_emails" message: "all email addresses must contain '@'" expression: "this.all(email, email.contains('@'))" }];}For more on all(), see all on CEL by Example.
exists() macro
Section titled “exists() macro”The exists() macro returns true when at least one element in a list satisfies a predicate. It returns false for empty lists.
// At least one number is negative.[1, -2, 3].exists(n, n < 0) // true
// No elements means no match.[].exists(n, n < 0) // falseFor simple membership checks, prefer the in operator over exists(). Use exists() when the condition is more complex than equality:
import "buf/validate/validate.proto";
message TeamRoster { // At least one player must be designated as captain. repeated Player players = 1 [(buf.validate.field).cel = { id: "roster.has_captain" message: "at least one player must be a captain" expression: "this.exists(p, p.is_captain)" }];}
message Player { string name = 1; bool is_captain = 2;}For more on exists(), see exists on CEL by Example.
filter() macro
Section titled “filter() macro”The filter() macro creates a new list containing only the elements that satisfy a predicate. Combine it with size() to count how many elements match a condition.
// Keep only even numbers.[1, 2, 3, 4].filter(n, n % 2 == 0) // [2, 4]
// Count elements matching a condition.[1, 2, 3, 4].filter(n, n % 2 == 0).size() // 2In Protovalidate rules, filter() is useful for counting elements that satisfy a condition:
import "buf/validate/validate.proto";
message SurveyResponse { // At least three answers must be non-empty. repeated string answers = 1 [(buf.validate.field).cel = { id: "survey.min_answers" message: "at least three questions must be answered" expression: "this.filter(a, a.size() > 0).size() >= 3" }];}For more on filter(), see filter on CEL by Example.
has() macro for field presence
Section titled “has() macro for field presence”The has() macro checks whether a field is set on a message. It is essential for validating optional fields and wrapper types where the absence of a value is meaningful.
// Check if a field is set.has(this.nickname) // true if nickname has been setHere is an example using has() to conditionally validate a field:
import "buf/validate/validate.proto";
message UpdateProfileRequest { string user_id = 1; // If a nickname is provided, it must be at least 2 characters. optional string nickname = 2; // If an age is provided, it must be between 1 and 150. optional uint32 age = 3;
option (buf.validate.message).cel = { id: "profile.nickname_length" message: "nickname must be at least 2 characters" expression: "!has(this.nickname) || this.nickname.size() >= 2" }; option (buf.validate.message).cel = { id: "profile.age_range" message: "age must be between 1 and 150" expression: "!has(this.age) || (this.age >= 1u && this.age <= 150u)" };}For more on has(), see has on CEL by Example.
Map validation
Section titled “Map validation”When iterating over a map with macros like all() or exists(), CEL iterates over the map's keys by default. Access the corresponding values through the map using bracket notation.
// Check that all values in a map are positive.{"a": 1, "b": 2}.all(key, {"a": 1, "b": 2}[key] > 0) // trueIn Protovalidate rules, use this to reference the map when accessing values within predicates:
import "buf/validate/validate.proto";
message FeatureFlags { // Every flag description must be non-empty. map<string, string> flags = 1 [(buf.validate.field).cel = { id: "flags.descriptions_non_empty" message: "every feature flag must have a non-empty description" expression: "this.all(key, this[key].size() > 0)" }];}Learn more
Section titled “Learn more”- Collections on CEL by Example: Overview of CEL's list and map types.
- all on CEL by Example: The
all()macro in detail. - exists on CEL by Example: The
exists()macro in detail. - filter on CEL by Example: The
filter()macro in detail. - has on CEL by Example: The
has()macro in detail. - CEL extensions reference: Protovalidate-specific functions like
unique(). - List examples in custom rules: More list validation patterns.