The structure of a .proto file is itself defined in Protobuf, in a file called descriptor.proto that ships with the compiler. When the compiler reads your schema, it produces descriptors, which are messages of those types that describe every file, message, and field. Reflection, dynamic decoding, validation, and compiler plugins all work from descriptors.
Schemas Describing Schemas
The compiler can write your schema out as a FileDescriptorSet with buf build -o descriptor.binpb or protoc --descriptor_set_out. That output is an ordinary Protobuf message, defined by descriptor.proto, so the same tools that work with any other message can read and analyze a schema at runtime. Buf's descriptors reference goes through the details.
Why is this useful?
Dynamic Decoding
Tools like the site's Binary Explorer use descriptors to decode arbitrary binary data without generating static code.
Validation
Complex rule engines (like protovalidate) use descriptors to apply constraints dynamically.
Code Generation
Code generator plugins receive these descriptors as input. Every custom generator is built on them, and the same plugin binary works under both buf generate and protoc.
Server Reflection
A server can send its own descriptors to clients over server reflection, so a client can call it without having the schema ahead of time. buf curl uses this to invoke any reflection-enabled gRPC or Connect endpoint with no .proto files on your disk.
Try editing the schema below to see how the generated FileDescriptorSet changes in real-time.
edition = "2024"; package demo.v1; import "buf/validate/validate.proto"; message User { string id = 1 [json_name = "uid"]; string name = 2; uint32 age = 3 [(buf.validate.field).uint32.lt = 150]; Role role = 4; enum Role { ROLE_UNSPECIFIED = 0; ROLE_USER = 1; ROLE_ADMIN = 2; } }
Next
Extending
Plugins, extensions, and custom options. A plugin's input is the descriptors from this page.