Skip to content

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.

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() // 3

For 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"
};
}

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) // true

In 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.

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) // false

For 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.

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() // 2

In 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.

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 set

Here 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.

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) // true

In 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)"
}];
}