Skip to content

[Feature Request] Allow certain rules to be run only within certain contexts #486

Open

Description

@jeffsawatzky

Feature description:
For an API I am trying to design, I would like to support Create/Patch style requests that have exactly the same payloads, except the Create doesn't require an ID (and should reject the request if provided) and the Patch doesn't have any required fields other than ID.

Problem it solves or use case:
I don't want to have to maintain multiple separate message payloads, and the validation rules on each field across those messages, when the payloads are pretty much identical. There would be a lot of duplication across the validation rules, which can lead to issues down the line.

Proposed implementation or solution:
Allow a way to specify the contexts in which a rule should be applied, and then when you validate a message you can specify the context in which to validate.
A naive, not fully fleshed out, approach could be to create new rules that take the rule plus a list of contexts for which the rule would be applied:

edition = "2023";

package bufbuild.people.v1;

import "buf/validate/validate.proto";
import "bufbuild/people/v1/predefined_string_rules.proto";

message Person {
  string id = 1 [
    (buf.validate.field).ctx_required = {rule: true, ctx: "patch"} // Only required for Patch requests
  ];
  string name = 2 [
    (buf.validate.field).ctx_required = {rule: true, ctx: "create"}, // Only required for Create requests
    (buf.validate.field).string.min_len = 1, // Required for all requests
    (buf.validate.field).string.ctx_max_len = {rule:256, ctx: "create", ctx: "patch"} // Required for create/patch requests
  ];
}

The context ids above are free text, but maybe a hard coded enum, or an int so that user can use a custom defined enum might be better?
Then, when you call validate you could pass a single/list of contexts for which to validate against.

Contribution:
I can try to do some of this stuff once the schema has been defined and approved. I can help implement the python side of things of needed.

Activity

  1. rodaine commented on May 27, 2026

    @rodaine
    Member

    Thanks for the request, @jeffsawatzky! Some similar issue related to filtering #112. I see where you're going with contexts but the caller of validation needs to be context-aware, while the solution in #112 (both as a static field mask, or as another field mask field on the message) allows this information to be self-contained on the proto like the rest of the validation rules.

  2. jeffsawatzky commented on May 27, 2026

    @jeffsawatzky
    Author

    @rodaine the solution in #112 will skip all validation on a field. I just want to skip the required validation on a field, but if the field is provided I want it validated still. What if, instead of the context being provided at the time of the validation call, it was embedded in the proto message definition as a custom option? That way the call site doesn't need to be context-aware. And these custom context options could be combined with ones from child messages. That way my CreateRequest could specify a custom context option of create and my UpdateRequest could specify a custom context option of update and then those contexts are applied to those messages as well as any child messages.

  3. jeffsawatzky commented on May 27, 2026

    @jeffsawatzky
    Author

    @rodaine so if I were to update my example above, it would now look something like:

    edition = "2023";
    
    package bufbuild.people.v1;
    
    import "buf/validate/validate.proto";
    import "bufbuild/people/v1/predefined_string_rules.proto";
    
    message Person {
      string id = 1 [
        (buf.validate.field).ctx_required = {rule: true, ctx: "patch"} // Only required for Patch requests
      ];
      string name = 2 [
        (buf.validate.field).ctx_required = {rule: true, ctx: "create"}, // Only required for Create requests
        (buf.validate.field).string.min_len = 1, // Required for all requests
        (buf.validate.field).string.ctx_max_len = {rule:256, ctx: "create", ctx: "patch"} // Required for create/patch requests
      ];
    }
    
    message CreatePersonRequest {
      option (buf.validate.message).ctx = "create"; // Applies the context to the current message and all children
      Person person = 1;
    }
    
    message UpdatePersonRequest {
      Person person = 1 [(buf.validate.field).ctx = "update"]; // Applies the context to the field and all it's children
    }
  4. jeffsawatzky commented on May 27, 2026

    @jeffsawatzky
    Author

    @rodaine basically the solution in #112 runs all validations on a field if specified in a field mask. What I would like is a way to run a subset of validations in certain situations. Specifically the required validation, but if it can be generalized that would be great. The required validation is a bit different than all others because it is checked whether or not the field was specified, whereas all other validations only run when a field is specified.

    Also, if there was a way to specify the ctx in a message and then have that provided in custom cel expressions, then that might work too, though I think required still needs a special case since the custom cel expression would only be run when the field is specified.

    Maybe I could put the required field validation into a message cel expression and it could get the ctx and then validate the required fields depending on the ctx?

  5. jeffsawatzky commented on May 27, 2026

    @jeffsawatzky
    Author

    Maybe a simple skip_required option somewhere (I could set it in the Update request) to skip any required fields in the current and child fields would suffice for my needs? But the ctx solution opens up a lot more customization.

  6. jeffsawatzky commented on May 27, 2026

    @jeffsawatzky
    Author

    Another use case for the ctx solution would be if we wanted to perform validation on server responses too, to make sure that the server is returning valid responses. For example, if I have a field marked with (google.api.field_behavior) = OUTPUT_ONLY then I could create a validation that is only run for the UpdatePersonResponse message to make sure that output field on the Person is set for the response (maybe we include some metadata about the person in the response, like created/updated timestamps)

  7. rodaine commented on May 28, 2026

    @rodaine
    Member

    That way my CreateRequest could specify a custom context option of create and my UpdateRequest could specify a custom context option of update and then those contexts are applied to those messages as well as any child messages.

    ...

    Another use case for the ctx solution would be if we wanted to perform validation on server responses too, to make sure that the server is returning valid responses. For example, if I have a field marked with (google.api.field_behavior) = OUTPUT_ONLY then I could create a validation that is only run for the UpdatePersonResponse message to make sure that output field on the Person is set for the response (maybe we include some metadata about the person in the response, like created/updated timestamps)

    From your responses on #112, I suspect you're doing this to match AIP/AEP rules? If so, the response message on many of the standard RPCs is the resource message itself (i.e., the correct form is rpc UpdatePerson(UpdatePersonRequest) returns (Person)). How would you specify the context in those cases if you want to validate the response messages? For what it's worth, we internally perform validation on response messages for bug-detection as well at Buf.

    Maybe a simple skip_required option somewhere (I could set it in the Update request) to skip any required fields in the current and child fields would suffice for my needs? But the ctx solution opens up a lot more customization.

    There's some nuance here. required itself boils down to msg.has(field) returns true. However, other standard rules have this built-in by construction (said another way, the constraint is a subset of required) that will still apply for implicit-presence fields. For instance, string.uuid requires a particular shape that will always satisfy required. Disabling required in this case is effectively a no-op since an empty string cannot satisfy a UUID regardless. The only time these rules are independent is when a field has explicit presence (composite fields, proto2/3 optional scalar fields, oneof members, and explicit presence fields in editions, which is the default).

    Speaking of AIP/AEP, the field mask is required to do partial updates without ambiguity. When provided, the caller is specifying the exact fields that will be updated, and with #112, what fields need validation. This is required for implicit presence fields since there's no difference between unset and a zero value. Adding a context won't help with this distinction either.

  8. jeffsawatzky commented on May 28, 2026

    @jeffsawatzky
    Author

    No, I'm not using google's AIP, but I do think the update_mask FieldMask is a good idea and only validating what's in the mask.

    I am using edition 2024 so all fields (other than map/repeated) have explicit presence by default. So I need to use required to ensure required fields are present, otherwise other validators are skipped. I like the explicit presence because it allows patch style updates without having to generate a field mask (I use python and the field mask helpers are non-existent there)

    As for the responses that are the resource itself, I guess you can't really include a "response" ctx in that case. Are there ways to supply the ctx in the service method as an option? Though that would require changes to the connectrpc code generator or something. There's always the option of providing additional ctx at the time of the validate call, or if you need this functionality then wrap your response in a response message (goes against AIP, I know, but does follow other industry best practices like buf's own linter).

  9. jeffsawatzky commented on May 28, 2026

    @jeffsawatzky
    Author

    Also, I see validation as mainly a server side concern. Sure, clients can validate their requests before sending, but don't need to, and the server should always perform its own validation. So, if we need to supply ctx at the time of the validate call, it could be restricted to the server only, which would be in one spot, and isn't that bad (though I agree it would be nice to encode all this in the proto somehow)

  10. rodaine commented on May 28, 2026

    @rodaine
    Member

    You'd be surprised how many folks use pv for client-side validation! And yes, while protos are definitely strongly associated with RPCs, they have standalone uses outside of client/server interactions too. Protovalidate is agnostic to the usecase.

  11. gpoirier-cw commented on Sep 28, 2026

    @gpoirier-cw

    It would be kind of amazing if (buf.validate.field).ignore = IGNORE_IF_ZERO_VALUE extended into the referenced message like IGNORE_ALWAYS does. Would you be open to another option for ignore that did so since this would probably be a rough breaking change?

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    FeatureNew feature or request

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions