Skip to content
protobuf.kmcd.dev

Extending

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.

PLUGIN_ARCHITECTURE
# 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=true
I/O Architecture

The compiler starts the plugin program as a subprocess.

  • stdin: The compiler passes a binary serialized CodeGeneratorRequest message.
  • stdout: The plugin must return a binary serialized CodeGeneratorResponse message. 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.
Request & Response Details

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.

Extension Numbers are Field Numbers

Under the hood, extension numbers are standard field numbers. Because they occupy tag space in the serialized message, you must ensure that no two extensions targeting the same message use the same number, as this would result in collisions and data corruption.

BASE.PROTO (DEFINITION)
edition = "2023";

message UserProfile {
  string username = 1;

  // Declare range of tags reserved for third-party extensions
  extensions 100 to 199;
}
BILLING.PROTO (EXTENSION)
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

FileMessageFieldOneofEnumEnumValueServiceMethod

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.

Extension Registries

Because extensions are defined globally for a descriptor (like FieldOptions), you must ensure your field numbers don't conflict with others.

Google maintains a Global Extension Registry for public projects.

OPTIONS.PROTO (DEFINITION)
edition = "2023";
import "google/protobuf/descriptor.proto";

extend google.protobuf.FieldOptions {
  bool is_pii = 50001;
}

extend google.protobuf.MethodOptions {
  string required_role = 50002;
}
SERVICE.PROTO (USAGE)
edition = "2023";
import "options.proto";

service UserService {
  rpc GetSensitiveData(GetRequest) returns (GetResponse) {
    option (required_role) = "ADMIN";
  }
}

message Profile {
  string ssn = 1 [(is_pii) = true];
}