Skip to content

Reflection API#

The Buf Reflection API downloads a FileDescriptorSet for any module on the BSR over Connect, so a runtime tool can decode Protobuf bytes without static bindings for the messages it sees.

Note

The Reflection API at buf.reflect.v1beta1 is still in beta and may change. For a stable RPC surface that returns the same kind of payload, see buf.registry.module.v1.FileDescriptorSetService.

Download the schema for the Eliza demo module
$ curl \
    https://buf.build/buf.reflect.v1beta1.FileDescriptorSetService/GetFileDescriptorSet \
    -H "Content-Type: application/json" \
    -d '{"module": "buf.build/connectrpc/eliza"}'

The response is a FileDescriptorSet that describes every file in the requested module, along with every imported file the module’s types transitively depend on: every service, message, field, enum, and option.

Why reflection#

Without a schema, Protobuf binary data can’t be decoded meaningfully: fields are identified by tag numbers, and the wire format reuses encoding strategies across types, so a sequence of bytes alone can’t tell you whether a value is a string, a binary blob, or a nested message. Three places where that matters and code typically can’t depend on the schema at compile time:

  • Inspecting RPC traffic. Tools like tcpdump, Wireshark, and proxies need a schema to render encoded payloads as something a human can read.
  • Inspecting stored payloads. Messages on a queue or in a durable store may have been produced over a long stretch of time, against many versions of an evolving schema.
  • Data-pipeline transforms. A pipeline that validates blobs as a known type, drops sensitive fields, or converts to JSON for a downstream sink needs the schema for whatever message it’s looking at, fresh enough to know about new fields.

For an end-to-end Go library that wraps the API for these use cases, see Prototransform.

Call the API#

The API ships as a single Connect service:

Service buf.reflect.v1beta1.FileDescriptorSetService
Method GetFileDescriptorSet
Module reference buf.build/bufbuild/reflect
GitHub bufbuild/reflect-api
Reference docs buf.reflect.v1beta1.FileDescriptorSetService

Generate a Connect or gRPC client from buf.build/bufbuild/reflect, install one of its generated SDKs, or call the service over plain HTTP+JSON as in the curl example above. For full HTTP request shape options, see Calling BSR APIs.

The module field is the BSR module name in the form REMOTE/OWNER/REPOSITORY. For example, buf.build/connectrpc/eliza resolves on the public BSR (buf.build) under the connectrpc organization, repository eliza.

version is optional; omitted, it returns the latest commit on the module’s default label. The same value forms accepted by deps: in buf.yaml work here: a commit ID, a label name (default label is main), or the empty value for latest-on-default. For the full picture, see Dependency management.

Authentication#

Public modules don’t require a BSR token; the example above works without one. For private modules, supply a token via Authorization: Bearer ${BUF_TOKEN}; see Authentication.

Filter by symbol#

For large modules where you only need part of the schema, the symbols field prunes the returned FileDescriptorSet to a specific service, message, or other named entity (and the dependencies it transitively references). Pass an array of fully qualified names.

Download only the google.longrunning.Operations service from googleapis
$ curl \
    https://buf.build/buf.reflect.v1beta1.FileDescriptorSetService/GetFileDescriptorSet \
    -H "Content-Type: application/json" \
    -d '{
          "module": "buf.build/googleapis/googleapis",
          "version": "75b4300737fb4efca0831636be94e517",
          "symbols": ["google.longrunning.Operations"]
        }'

Symbol filtering can shrink the response by roughly an order of magnitude. The example call above returns a FileDescriptorSet around 11 KB; the same call without the symbols field returns about 10x that for googleapis/googleapis.

What you do with a FileDescriptorSet#

A FileDescriptorSet is the runtime equivalent of generated code: build a “rich” descriptor index from it, then construct dynamic messages backed by those descriptors instead of by static types. A dynamic message can marshal and parse Protobuf bytes the same as a generated message, but its fields are resolved through the descriptor at runtime, which lets one program handle messages of any type.

For Go, Prototransform wraps the watcher, descriptor, dynamic-message, and conversion plumbing into one library. For other languages, the underlying primitives are part of the official Protobuf runtimes:

Language Descriptors Dynamic messages
C++ google::protobuf::Descriptor google::protobuf::DynamicMessage
Go protoreflect dynamicpb
Java com.google.protobuf.Descriptors com.google.protobuf.DynamicMessage
Python descriptor_pool message_factory

C# and PHP expose descriptor reflection but don’t ship dynamic-message support; for a Wireshark- or pipeline-style appliance, those runtimes need a third-party library or a different language.

For background on the descriptor model itself, see the Descriptors reference.

Further reading#