05_EXTENDING
Extending
Protobuf can be extended at compile time and at schema-definition time. These sections cover plugins, extensions, and custom options.
The protoc (or buf generate) compiler doesn't actually know how to generate code for Go, Java, or TypeScript. Instead, it parses the .proto files and hands the resulting Descriptors to a plugin.
This architecture allows anyone to write a plugin to generate a wide range of outputs, such as documentation, client libraries, or even SQL schemas, from a Protobuf definition. For more information, see the plugin.proto file itself.
# Example: Running a local plugin with buf generate (buf.gen.yaml)
version: v2
plugins:
- local: ./my-plugin
out: ./generated
opt: log_level=debug,other_flag=trueThe compiler starts the plugin program as a subprocess.
- stdin: The compiler passes a binary serialized
CodeGeneratorRequestmessage. - stdout: The plugin must return a binary serialized
CodeGeneratorResponsemessage. The plugin must not modify the filesystem directly; it returns the files to be written in this response. - stderr: Used strictly for logging and errors. Any logging should be disabled by default and controlled by a CLI flag to keep the output clean.
Flags & Parameters: Any options passed via --<plugin>_opt are provided to the plugin in the parameter field of the CodeGeneratorRequest as a single comma-separated string. The plugin is responsible for parsing and splitting this string.
What to generate: The compiler passes many files (including dependencies), but the plugin must only generate code for the files listed in the file_to_generate field of the request.
Required Features: In the CodeGeneratorResponse, you are heavily encouraged to explicitly declare your supported features. Setting supported_features along with minimum_edition and maximum_edition is essentially required, as users cannot compile modern Protobuf Editions using your plugin without them.
Protobuf extensions allow you to declare that a message has a range of field numbers reserved for external usage. Third parties can then define new fields for that message without modifying the original file.
How extension support differs across versions:
proto2: Allows extensions on any message (both user-defined messages and standard options).proto3: Restricts extensions exclusively to option messages (specifically to define custom options; more on that later).- Editions: Restores the ability to extend any message (bringing back general-purpose extensions) while keeping option definitions standard and native.
To use extensions in proto2 or Editions, you must define an extension range in the base message using the extensions keyword. External files can then declare fields targeting that range.
edition = "2023"; message UserProfile { string username = 1; // Declare range of tags reserved for third-party extensions extensions 100 to 199; }
edition = "2023"; import "base.proto"; // Extend the custom UserProfile message directly extend UserProfile { optional string stripe_customer_id = 100; }
You can define custom "options" (annotations) to attach metadata to your schema. Common use cases include defining data validation rules (e.g., protovalidate), field-level data classification (e.g., tagging PII), and service-level access control (e.g., defining required roles for RBAC).
These annotations are preserved in the binary descriptors, which makes them accessible to anything that processes your schema. This includes protoc plugins that generate custom code, systems that configure themselves during startup, or dynamic tools that load and inspect schemas on demand via reflection.
Under the hood, custom options are defined by using the extend keyword to target the built-in option descriptor messages (like FieldOptions or MethodOptions).
Available Scopes
Metadata can be attached to any of these points by extending the respective standard descriptor messages.
For more information, see the Editions Custom Options Guide.
edition = "2023"; import "google/protobuf/descriptor.proto"; extend google.protobuf.FieldOptions { bool is_pii = 50001; } extend google.protobuf.MethodOptions { string required_role = 50002; }
edition = "2023"; import "options.proto"; service UserService { rpc GetSensitiveData(GetRequest) returns (GetResponse) { option (required_role) = "ADMIN"; } } message Profile { string ssn = 1 [(is_pii) = true]; }