Skip to content

Custom CEL rules#

Use custom rules and Common Expression Language (CEL) to compare multiple fields or write validation logic that standard rules can't express.

Example#

CEL rules validate constraints using CEL's simple expression language:

CEL example
import "buf/validate/validate.proto";

message BeverageMenu {
  option (buf.validate.message).cel = {
    id: "daily_special_either_hot_or_cold"
    message: "the daily special must be in either the hot menu or the cold menu"
    // The `+` is overloaded for lists (repeated fields) and it concatenates two lists.
    // In this case `this.hot_beverages` + `this.cold_beverages` evaluates to a list
    // containing beverages from both lists.
    expression: "this.daily_special in this.hot_beverages + this.cold_beverages"
  };

  repeated string hot_beverages = 1;
  repeated string cold_beverages = 2;
  string daily_special = 3;
}

Basics of CEL expressions#

With CEL, you add validation logic to your schemas using its JavaScript-like syntax:

  • this < 100: Check that an int32 is less than 100.
  • this != 'localhost': Check that a string isn't localhost.

CEL functions#

Your expressions can use the common library of CEL functions and Protovalidate's own extension functions:

  • !this.isInf(): A double can't be infinity.
  • this.isHostname(): A string must be a valid hostname.
  • this <= duration('23h59m59s'): A Duration must be less than a day.

Message-level CEL#

When you need to compare multiple fields, message-level CEL lets you access any field in a message:

  • this.min_bedroom_count <= this.max_bedroom_count: When searching for an apartment, the minimum bedroom value must be less than the maximum bedroom value.
  • this.require_even == false || size(this.numbers.filter(i, i % 2 == 0)) > 0: Combining multiple fields, simple CEL functions, and advanced functions like filter(), require that one number in a repeated int32 is even, but only when require_even is true.

Custom field rules#

Custom field rules use (buf.validate.field).cel's three fields:

  • id: A unique (within the field) identifier for this rule.
  • message: An optional, human-readable message to return when this rule fails.
  • expression: A CEL expression to evaluate, returning either a bool or a string. If a non-empty string is returned, validation fails and the returned string overrides any message.

Within field-level custom rules, this refers to the value of the field.

Example:

Custom field rule example
import "buf/validate/validate.proto";

message PlaceWholeSaleOrderRequest {
  // item_id is the ID of the item to purchase.
  uint64 item_id = 1;
  // quantity is the quantity of the item to purchase.
  uint32 quantity = 2 [(buf.validate.field).cel = {
    id: "minimum_whole_sale_quantity"
    message: "order quantity must be 100 or greater"
    // `this` refers to this field and the expression evaluates to a boolean result.
    // If the result is false, validation will fail with the above error message.
    expression: "this >= 100"
  }];
}

Combining field rules#

Just like standard rules, you can freely combine custom rules with other custom or standard rules:

Combining custom and standard rules
import "buf/validate/validate.proto";

message DeviceInfo {
    string hostname = 1 [
        // Required: minimum length of one.
        (buf.validate.field).string.min_len = 1,

        // The value must be a valid hostname.
        (buf.validate.field).cel = {
            id: "hostname.ishostname"
            message: "hostname must be valid"
            expression: "this.isHostname()"
        },

        // Reject "localhost" as invalid.
        (buf.validate.field).cel = {
            id: "hostname.notlocalhost"
            message: "localhost is not permitted"
            expression: "this != 'localhost'"
        }
    ];
}

Custom message rules#

Message rules access multiple fields, so you can validate constraints like "start_date must be before end_date."

As an example, multiple field values can be combined with CEL functions to create a validation rule requiring that a request for an indirect flight doesn't result in a trip longer than 48 hours:

Example multi-field message rule
import "buf/validate/validate.proto";

message IndirectFlightRequest {
    // The sum of both flight durations and the layover must not exceed
    // a maximum duration of 48 hours.
    option (buf.validate.message).cel = {
        id: "trip.duration.maximum"
        message: "the entire trip must be less than 48 hours"
        expression:
            "this.first_flight_duration"
            "+ this.second_flight_duration"
            "+ this.layover_duration < duration('48h')"
    };

    google.protobuf.Duration first_flight_duration = 1;
    google.protobuf.Duration layover_duration = 2;
    google.protobuf.Duration second_flight_duration = 3;
}

Message rules have a few differences from field rules:

  1. Their id values must be unique within the message.
  2. this refers to the message itself. Properties within the message can be accessed via dot notation.

Combining message rules#

Multiple message-level rules can be combined, and their CEL expressions can access properties of nested messages, traversing graphs of related messages.

This example combines multiple rules with conditional logic, validating apartment occupancy against zoning regulations when present.

Try changing occupants to 4 or removing the occupancy_rules and you'll see that the sample message becomes valid.

Working with fields#

Access field values#

Reference a field value with this. In this example, wholesale orders must have a quantity of at least 100.

import "buf/validate/validate.proto";

message PlaceWholeSaleOrderRequest {
  uint64 item_id = 1;
  uint32 quantity = 2 [(buf.validate.field).cel = {
    id: "minimum_whole_sale_quantity"
    message: "order quantity must be 100 or greater"
    expression: "this >= 100"
  }];
}

Try it in the Playground ↗

Compare field values#

Use message-level rules to compare multiple fields. Example: when searching for apartments, the minimum bedroom count cannot exceed the maximum bedroom count.

import "buf/validate/validate.proto";

message SearchApartmentRequest {
  option (buf.validate.message).cel = {
    id: "min_bedrooms_lte_max_bedrooms"
    message: "the minimum number of bedrooms cannot be higher than the maximum number"
    expression:
      "!has(this.max_bedroom_count) ? true "
      ": !has(this.min_bedroom_count) ? true "
      ": this.min_bedroom_count <= this.max_bedroom_count"
  };

  uint32 max_bedroom_count = 1;
  uint32 min_bedroom_count = 2;
}

Try it in the Playground ↗

Check field presence#

has() checks if optional fields are set. Example: you cannot set a secondary residence without also setting a primary residence.

import "buf/validate/validate.proto";

message UpdateAddressInfoRequest {
  option (buf.validate.message).cel = {
    id: "secondary_residence_depends_on_primary"
    expression:
      "has(this.new_secondary_residence) && !has(this.new_primary_residence)"
      "? 'cannot set a secondary residence without setting a primary one'"
      ": ''"
  };

  google.type.PostalAddress new_primary_residence = 1;
  google.type.PostalAddress new_secondary_residence = 2;
}

.has() can also check presence for deeply nested fields. For example, this validates that a contact must have a first name by checking the entire nested path.

import "buf/validate/validate.proto";

message CreateContactRequest {
  option (buf.validate.message).cel = {
    id: "create_contact_with_first_name"
    message: "the contact must have a first name"
    expression: "has(this.contact.full_name.first_name)"
  };

  Contact contact = 1;
}

message Contact {
  FullName full_name = 1;
}

message FullName {
  string first_name = 1;
  string last_name = 2;
}

Try it in the Playground ↗

Scalar examples#

The following examples work with all scalar types: strings, numbers, bytes, bools, and Google's wrapper types.

The in operator#

The in operator checks if a value exists in a list.

import "buf/validate/validate.proto";

message IsDivisibleRequest {
  int32 dividend = 1;
  int32 divisor = 2 [(buf.validate.field).cel = {
    id: "divisor_must_be_a_small_prime"
    message: "the divisor must be one of 2, 3 and 5"
    expression: "this in [2, 3, 5]"
  }];
}

Try it in the Playground ↗

Type conversion#

string(), uint(), int(), bytes(), and double() convert between types.

import "buf/validate/validate.proto";

message Sound {
  uint32 frequency_hz = 1 [(buf.validate.field).cel = {
    id: "frequency_in_the_audible_range"
    expression:
      "this < 20 ? string(this) + ' hz is too low for human ears'"
      ": this > 20000 ? string(this) + ' hz is too high for human ears'"
      ": ''"
  }];
}

Try it in the Playground ↗

Strings#

Contains#

.contains() checks if a string contains a substring. In this example, it verifies all keywords appear in the article content.

import "buf/validate/validate.proto";

message PublishArticleRequest {
  option (buf.validate.message).cel = {
    id: "article_contain_keyword"
    message: "article must contain all the keywords it is tagged with"
    expression: "this.keyword.all(kw, this.article_content.contains(kw))"
  };

  string article_content = 1;
  repeated string keyword = 2;
}

Try it in the Playground ↗

Starts with / ends with#

.startsWith() and .endsWith() check for string prefixes and suffixes. Example: Jeopardy answers must start with 'wh' and end with '?'.

import "buf/validate/validate.proto";

message AnswerJeopardyQuestionRequest {
  uint64 question_id = 1;
  string answer = 2 [(buf.validate.field).cel = {
    id: "correct_answer_format"
    message: "answer must start with 'wh' and end with '?'"
    expression: "this.startsWith('wh') && this.endsWith('?')"
  }];
}

Try it in the Playground ↗

Pattern matching#

.matches() tests strings against regex patterns. In this example, usernames must be 3-16 characters and contain only letters and digits.

import "buf/validate/validate.proto";

message UpdateUsernameRequest {
  string new_username = 1 [(buf.validate.field).cel = {
    id: "username_format"
    message: "username must be 3 - 16 characters long and only contain letters and digits"
    expression: "this.matches('^[A-Za-z0-9]{3,16}$')"
  }];
}

Try it in the Playground ↗

Email validation#

.isEmail() checks if a string is a valid email address.

import "buf/validate/validate.proto";

message AddEmailToMailingListRequest {
  string email = 1 [(buf.validate.field).cel = {
    id: "valid_email"
    message: "email must be a valid email"
    expression: "this.isEmail()"
  }];
}

Try it in the Playground ↗

Hostname validation#

.isHostname() checks if a string is a valid hostname.

import "buf/validate/validate.proto";

message DeviceInfo {
  string hostname = 1 [(buf.validate.field).cel = {
    id: "device_info_valid_hostname"
    message: "hostname must be valid"
    expression: "this.isHostname()"
  }];
}

Try it in the Playground ↗

IP address validation#

.isIp(), .isIp(4), .isIp(6), and .isIpPrefix() check if strings are valid IP addresses and prefixes.

import "buf/validate/validate.proto";

message LocationForIpRequest {
  string ip_address = 1 [(buf.validate.field).cel = {
    id: "valid_address"
    message: "ip_address must be a valid IP address"
    expression: "this.isIp()"
  }];
}

Try it in the Playground ↗

import "buf/validate/validate.proto";

message LocationForIpPrefixRequest {
  string ip_prefix = 1 [(buf.validate.field).cel = {
    id: "valid_prefix"
    message: "ip_prefix must be a valid IP with prefix length"
    expression: "this.isIpPrefix()"
  }];
}

Try it in the Playground ↗

URI validation#

.isUri() checks if a string is a valid absolute URI. Use .isUriRef() to also allow relative URIs.

import "buf/validate/validate.proto";

message UploadResourceRequest {
  string uri = 1 [(buf.validate.field).cel = {
    id: "valid_uri"
    message: "uri must be a valid URI"
    expression: "this.isUri()"
  }];
  bytes data = 2;
}

Try it in the Playground ↗

Concatenation#

The + operator can concatenate strings. For example, it can be used to build dynamic error messages.

import "buf/validate/validate.proto";

message JoinNewsLetterRequest {
  string email = 1 [(buf.validate.field).cel = {
    id: "join_news_letter_request_valid_email"
    expression:
      "this.isEmail() ? ''"
      ": '\"' + this + '\" is not a valid email'"
  }];
}

Try it in the Playground ↗

Bytes examples#

Concatenation#

The + operator can be used to concatenate bytes. size() returns the length in bytes.

import "buf/validate/validate.proto";

message CompactDocument {
  option (buf.validate.message).cel = {
    id: "header_footer_size_limit"
    message: "header and footer should be less than 500 bytes in total"
    expression: "size(this.header + this.footer) < 500"
  };

  bytes header = 1;
  bytes footer = 2;
}

Try it in the Playground ↗

Contains#

.contains() checks if a byte sequence contains another sequence. Use bytes() to convert strings to bytes.

import "buf/validate/validate.proto";

message Application {
  bytes binary = 1 [(buf.validate.field).cel = {
    id: "without_malicious_code"
    message: "binary should not contain malicious code"
    expression: "!this.contains(bytes('malicious code'))"
  }];
}

Try it in the Playground ↗

Starts with / ends with#

.startsWith() and .endsWith() check for byte sequence prefixes and suffixes. Example: script files must start with a shebang and end with a newline.

import "buf/validate/validate.proto";

message ScriptFile {
  bytes content = 1 [(buf.validate.field).cel = {
    id: "script_start_with_shabang_end_with_line_feed"
    expression:
      "!this.startsWith(bytes('#!')) ? 'must start with #!'"
      ": !this.endsWith(bytes('\\x0A')) ? 'must end with a new line'"
      ": ''"
  }];
}

Try it in the Playground ↗

Numeric examples#

Arithmetic operators#

Arithmetic operators (+, -, *, /, %) perform mathematical operations.

import "buf/validate/validate.proto";

message FiveDigitPrimeLookalike {
  int32 value = 1 [(buf.validate.field).cel = {
    id: "prime_lookalike"
    message: "value must have 5 digits and look like a prime"
    expression:
      "this % 2 != 0"
      "&& this % 3 != 0"
      "&& this % 5 != 0"
      "&& (this - 10000) * (this - 99999) <= 0"
      "&& -this != 77777"
  }];
}

Try it in the Playground ↗

Check for infinity#

.isInf() checks if a float or double is infinite.

import "buf/validate/validate.proto";

message SetBalanceRequest {
  double new_balance = 1 [(buf.validate.field).cel = {
    id: "finite_balance"
    message: "balance should be finite"
    expression: "!this.isInf()"
  }];
}

Try it in the Playground ↗

Timestamps & durations#

Timestamp comparison#

Comparison operators (<=, <, >, >=, ==, !=) compare timestamps. timestamp() creates timestamps from RFC3339-formatted strings.

import "buf/validate/validate.proto";

message EventFromTheNineteenthCentury {
  google.protobuf.Timestamp time = 1 [(buf.validate.field).cel = {
    id: "timestamp_in_the_1800s"
    message: "the event must be from the nineteenth century"
    expression:
      "timestamp('1800-01-01T00:00:00+00:00') <= this"
      "&& this < timestamp('1900-01-01T00:00:00+00:00')"
  }];
}

Try it in the Playground ↗

Timestamp attributes#

.getDayOfWeek(), .getDate(), .getDayOfMonth(), .getDayOfYear(), .getFullYear(), .getHours(), .getMinutes(), .getSeconds(), and .getMilliseconds() extract date/time components.

import "buf/validate/validate.proto";

message BookHaircutAppointmentRequest {
  google.protobuf.Timestamp appointment_time = 1 [(buf.validate.field).cel = {
    id: "not_open_on_monday"
    message: "the barbershop is closed on Monday"
    expression: "this.getDayOfWeek() != 1"
  }];
}

Try it in the Playground ↗

Timestamp plus duration#

Adding a timestamp and a duration produces a new timestamp.

import "buf/validate/validate.proto";

message UpdateFlightInfoRequest {
  option (buf.validate.message).cel = {
    id: "correct_duration"
    message: "duration must be the period of time between departure and arrival"
    expression: "this.new_departure_time + this.new_duration == this.new_arrival_time"
  };

  uint64 flight_id = 1;
  google.protobuf.Timestamp new_departure_time = 2;
  google.protobuf.Timestamp new_arrival_time = 3;
  google.protobuf.Duration new_duration = 4;
}

Try it in the Playground ↗

Timestamp subtraction#

Subtracting two timestamps produces a duration. now evaluates to the current timestamp.

import "buf/validate/validate.proto";

message BookReservationRequest {
  google.protobuf.Timestamp start_time = 1 [(buf.validate.field).cel = {
    id: "book_24_hrs_ahead"
    message: "must book at least 24 hours ahead"
    expression: "duration('24h') <= this - now"
  }];
}

Try it in the Playground ↗

Duration arithmetic#

The + operator adds durations together. Durations can be compared with comparison operators.

import "buf/validate/validate.proto";

message IndirectFlight {
  option (buf.validate.message).cel = {
    id: "total_length_limit"
    message: "the entire trip should be less than 48 hours"
    expression:
      "this.first_flight_duration + this.second_flight_duration"
      "+ this.layover_duration < duration('48h')"
  };

  google.protobuf.Duration first_flight_duration = 1;
  google.protobuf.Duration layover_duration = 2;
  google.protobuf.Duration second_flight_duration = 3;
}

Try it in the Playground ↗

Duration from string#

duration() creates durations from string literals. Supported suffixes: "h", "m", "s", "ms", "us", "ns".

import "buf/validate/validate.proto";

message StartTimerRequest {
  google.protobuf.Duration duration = 1 [(buf.validate.field).cel = {
    id: "maximum_duration"
    message: "timer duration must be shorter than a day"
    expression: "this <= duration('23h59m59s')"
  }];
}

Try it in the Playground ↗

List examples#

Concatenation#

The + operator concatenates lists (repeated fields).

import "buf/validate/validate.proto";

message BeverageMenu {
  option (buf.validate.message).cel = {
    id: "daily_special_either_hot_or_cold"
    message: "the daily special must be in either the hot menu or the cold menu"
    expression: "this.daily_special in this.hot_beverages + this.cold_beverages"
  };

  repeated string hot_beverages = 1;
  repeated string cold_beverages = 2;
  string daily_special = 3;
}

Try it in the Playground ↗

All elements match#

.all() checks if a predicate is true for all elements in a list.

import "buf/validate/validate.proto";

message CreateGroupChatRequest {
  option (buf.validate.message).cel = {
    id: "group_chat_member_must_be_verified"
    message: "all group chat members must be verified users"
    expression: "this.member.all(m, m.is_verified)"
  };

  repeated ChatAppUser member = 1;
}

message ChatAppUser {
  uint64 user_id = 1;
  bool is_verified = 2;
}

Try it in the Playground ↗

Exactly one element matches#

.exists_one() checks if exactly one element in a list satisfies a predicate.

import "buf/validate/validate.proto";

message TeamRoster {
  option (buf.validate.message).cel = {
    id: "captain_exists"
    message: "captain must be among the list of players"
    expression: "this.players.exists_one(p, p.name == this.captain_name)"
  };

  message Player {
    string name = 1;
    uint32 jersey_number = 2;
  }

  repeated Player players = 1;
  string captain_name = 2;
}

Try it in the Playground ↗

Filter and count#

.filter() creates a new list containing only elements that satisfy a predicate. size() returns the number of elements.

import "buf/validate/validate.proto";

message FiveTruthsThreeLies {
  option (buf.validate.message).cel = {
    id: "exactly_three_lies"
    message: "there must be exactly three lies"
    expression: "size(this.statement.filter(s, !s.is_truth)) == 3"
  };
  option (buf.validate.message).cel = {
    id: "exactly_five_truths"
    message: "there must be exactly five truths"
    expression: "size(this.statement.filter(s, s.is_truth)) == 5"
  };

  repeated TruthOrLieStatement statement = 1;
}

message TruthOrLieStatement {
  string statement = 1;
  bool is_truth = 2;
}

Try it in the Playground ↗

Map, unique, and nested messages#

.map() transforms each element in a list. .unique() checks if all elements are unique. Combine them to check uniqueness of properties in nested messages.

import "buf/validate/validate.proto";

message Book {
  repeated string genres = 1 [(buf.validate.field).cel = {
    id: "book_unique_genre"
    message: "a genre cannot appear twice in the same book"
    expression: "this.unique()"
  }];
  repeated Author authors = 2 [(buf.validate.field).cel = {
    id: "book_unique_authors"
    message: "a name cannot appear twice in authors"
    expression: "this.map(author, author.name).unique()"
  }];
}

message Author {
  string name = 1;
}

Try it in the Playground ↗

Validating items with CEL#

Apply CEL validation to individual items in a repeated field using the items constraint.

import "buf/validate/validate.proto";

message IPAllowlist {
  repeated string allow_cidr = 1 [(buf.validate.field).repeated = {
    min_items: 1
    items: {
      cel: [
        {
          id: "ip_prefix"
          message: "value must be IPv4 prefix"
          expression: "this.isIpPrefix(4, true)"
        }
      ]
    }
  }];
}

Try it in the Playground ↗

Map examples#

All entries match#

.all() checks if a predicate is true for all keys in a map. Access values using bracket notation.

import "buf/validate/validate.proto";

message ShoppingCart {
  option (buf.validate.message).cel = {
    id: "quantity_positive"
    message: "each product in the shopping cart must have positive quantity"
    expression: "this.product_id_to_quantity.all(pid, this.product_id_to_quantity[pid] > 0)"
  };

  map<uint64, uint32> product_id_to_quantity = 1;
}

Try it in the Playground ↗

At least one entry matches#

.exists() checks if at least one key in a map satisfies a predicate. Access values with bracket notation in the predicate.

import "buf/validate/validate.proto";

message CustomerReviews {
  option (buf.validate.message).cel = {
    id: "highest_rating_exists"
    message: "highest_rating must be the highest from reviews"
    expression:
      "this.customer_id_to_review.exists("
      "  id,"
      "  this.customer_id_to_review[id].rating == this.highest_rating"
      ")"
  };

  map<fixed64, ProductReview> customer_id_to_review = 1;
  float highest_rating = 2;
}

message ProductReview {
  float rating = 1;
  string review = 2;
}

Try it in the Playground ↗

Exactly one entry matches#

.exists_one() checks if exactly one key in a map satisfies a predicate.

import "buf/validate/validate.proto";

message TeamRoles {
  option (buf.validate.message).cel = {
    id: "exactly_one_captain"
    message: "there must be exactly one captain"
    expression: "this.name_to_role.exists_one(name, this.name_to_role[name] == 1)"
  };

  map<string, TeamRole> name_to_role = 1;
}

enum TeamRole {
  TEAM_ROLE_UNSPECIFIED = 0;
  TEAM_ROLE_CAPTAIN = 1;
  TEAM_ROLE_PLAYER = 2;
}

Try it in the Playground ↗

Size#

size() returns the number of keys in a map.

import "buf/validate/validate.proto";

message PenInventory {
  option (buf.validate.message).cel = {
    id: "at_most_30_colors"
    message: "there must not be more than 30 colors"
    expression: "size(this.color_to_count) <= 30"
  };

  map<uint32, uint32> color_to_count = 1;
}

Try it in the Playground ↗

Validating keys and values#

Apply CEL validation to map keys and values separately using the keys and values constraints.

import "buf/validate/validate.proto";

message IPAddressMapping {
  map<string, string> allow_cidr_map = 15 [(buf.validate.field).map = {
    keys: {
      cel: [
        {
          id: "ip_prefix"
          message: "key must be IPv4 prefix"
          expression: "this.isIpPrefix(4, true)"
        }
      ]
    }
    values: {
      cel: [
        {
          id: "ip_address"
          message: "value must be IPv4 address"
          expression: "this.isIpPrefix(4, true)"
        }
      ]
    }
  }];
}

Try it in the Playground ↗

Advanced examples#

Ternary expressions#

Nested ternary operators can be used to validate complex conditions. In this example, triangle points must be distinct and non-linear.

import "buf/validate/validate.proto";

message Triangle {
  option (buf.validate.message).cel = {
    id: "triangle.distinct_points"
    expression:
      "this.point_a == this.point_b ? 'point A and point B cannot be the same'"
      ": this.point_b == this.point_c ? 'point B and point C cannot be the same'"
      ": this.point_a == this.point_c ? 'point A and point C cannot be the same'"
      ": ''"
  };
  option (buf.validate.message).cel = {
    id: "triangle.non_linear_points"
    message: "three points must not be on the same line"
    expression:
      "(this.point_a.y - this.point_b.y) * (this.point_a.x - this.point_c.x)"
      "!= (this.point_a.y - this.point_c.y) * (this.point_a.x - this.point_b.x)"
  };

  Point point_a = 1;
  Point point_b = 2;
  Point point_c = 3;
}

message Point {
  double x = 1;
  double y = 2;
}

Try it in the Playground ↗

Field masks#

.all() checks if all field mask paths are in an allowed set.

import "buf/validate/validate.proto";

message UpdateSongRequest {
  Song song = 1;
  google.protobuf.FieldMask field_mask = 2 [(buf.validate.field).cel = {
    id: "valid_field_mask"
    message: "a field mask path must be one of name, duration and artist.name"
    expression: "this.paths.all(path, path in ['name', 'duration', 'artist.name'])"
  }];
}

message Song {
  string name = 1;
  fixed64 id = 2;
  google.protobuf.Timestamp create_time = 3;
  google.protobuf.Duration duration = 4;
  Artist artist = 5;
}

message Artist {
  string name = 1;
}

Try it in the Playground ↗

Enum comparison#

Compare enum values directly with integers, where the integer corresponds to the enum variant's numeric value.

import "buf/validate/validate.proto";

message UploadFileRequest {
  bytes content = 1;
  CompressionScheme compression_scheme = 2 [(buf.validate.field).cel = {
    id: "compression_specified"
    message: "compression scheme must be specified"
    expression: "this != 0"
  }];
}

enum CompressionScheme {
  COMPRESSION_SCHEME_UNSPECIFIED = 0;
  COMPRESSION_SCHEME_NONE = 1;
  COMPRESSION_SCHEME_GZIP = 2;
}

Try it in the Playground ↗

Value types#

type() checks the type of a google.protobuf.Value. Valid types include int, uint, double, bool, string, bytes, list, map, and null_type.

import "buf/validate/validate.proto";

message WriteKeyValuePairRequest {
  google.protobuf.Value key = 1 [(buf.validate.field).cel = {
    id: "write_key_value_pair_request_valid_key_type"
    message: "key must be one of int, uint, double, bool and string"
    expression: "type(this) in [int, uint, double, bool, string]"
  }];
  google.protobuf.Value value = 2;
}

Try it in the Playground ↗

Wrapper types#

Work with google.protobuf wrapper types. CEL rules on wrapper types only evaluate when the field is set. Use has() at the message level to check if wrapper fields are present.

import "buf/validate/validate.proto";

message SearchStoreRequest {
  option (buf.validate.message).cel = {
    id: "positive_quantity_but_out_of_stock"
    expression:
      "!has(this.in_stock) ? ''"
      ": !has(this.quantity_available) ? ''"
      ": this.in_stock ? ''"
      ": this.quantity_available > 0 ? 'cannot search for a positive quantity when filtering for out-of-stock items'"
      ": ''"
  };

  google.protobuf.StringValue term = 1 [(buf.validate.field).cel = {
    id: "search_term_length"
    message: "search term must not exceed 100 characters"
    expression: "size(this) <= 100"
  }];
  google.protobuf.UInt64Value category_id = 2 [(buf.validate.field).cel = {
    id: "category_id_not_123"
    message: "category id should not be 123"
    expression: "this != uint(123)"
  }];
  google.protobuf.BoolValue in_stock = 3;
  google.protobuf.FloatValue min_price = 4 [(buf.validate.field).cel = {
    id: "min_price_positive"
    message: "minimum price must be positive"
    expression: "this > 0"
  }];
  google.protobuf.DoubleValue max_price = 5 [(buf.validate.field).cel = {
    id: "max_price_finite"
    message: "maximum price must be finite"
    expression: "!this.isInf()"
  }];
  google.protobuf.UInt32Value quantity_available = 6;
}

Try it in the Playground ↗

Next steps#