PB reads standard protobuf FileDescriptorSet images — the same artifact any
protobuf toolchain can produce. There is no PB-specific protoc plugin and no
generated Elixir source.
With protoc
protoc --descriptor_set_out=schema.binpb --include_imports your.proto
--descriptor_set_outwrites the binaryFileDescriptorSet.--include_importsmakes the set self-contained by including imported.protofiles. Always pass it: PB needs every imported file, both to resolve the types it defines and to decode the custom options it defines, soPB.compile/2rejects a set that lacks one (see Missing imports).
Load and compile it:
{:ok, descriptor_set} = PB.decode_descriptor_set(File.read!("schema.binpb"))
schema = PB.compile(descriptor_set)A bang variant PB.decode_descriptor_set!/1 is also available.
With buf
If you use buf, produce a descriptor set (an "image") with:
buf build -o schema.binpb
buf images are FileDescriptorSet-compatible and load the same way. They
include the imported files unless you pass --exclude-imports, except those
imported with import option (see Missing imports).
Missing imports
PB.compile/2 raises a PB.SchemaError (kind: :missing_imports) when a file
in the set imports a file the set doesn't contain, listing every missing import:
PB failed to compile: descriptor set is missing imported files: "orders.proto"
imports "buf/validate/validate.proto". Build the set with its imports ...Without the imported file, the custom options it defines — such as the
buf.validate rules that pb_validate reads —
could only stay undecoded, so rebuilding the set with its imports is almost
always the fix. Weak imports may be missing.
So may the files an edition 2024 file imports with import option: buf build
never includes them, even without --exclude-imports. The options they define
then stay unknown fields (:__unknown_fields__) of the descriptor options. A set
built with protoc --include_imports includes them, and PB.compile/2 decodes
their options.
If the set deliberately leaves out imports that nothing needs, such as an
option-only import like google/api/annotations.proto whose HTTP annotations
you don't use, compile it with allow_missing_imports: true:
schema = PB.compile(descriptor_set, allow_missing_imports: true)use PB.Schema takes the same option. The options a missing file defines then
stay unknown fields (:__unknown_fields__) of the descriptor options, and a
field or service method whose type a missing file defines still fails to
compile, raising a PB.SchemaError that names the type.
Custom representation options in proto source
If you want to declare Elixir representation (struct:, unwrap:, identity
oneofs) directly in your .proto files rather than at compile time, import
elixir/pb/v1/options.proto and set the elixir.pb.v1 custom options. Those options
travel inside the descriptor set and are picked up by PB.compile/2. See
Decoding into structs for the option surface.
Embedding at compile time
Loading the descriptor set at runtime is fine for dynamic schemas. For a stable
schema, embed it into a module at compile time with use PB.Schema — see
Schema modules.