Open-source project
elixir-protobuf/protobuf avatar
elixir-protobuf/protobuf

protobuf-elixir: generating Elixir structs from .proto files with a protoc plugin

A pure Elixir implementation of Google Protobuf.

908 stars161 forksElixirMIT

At a glance

What is it?
protobuf-elixir is a pure Elixir implementation of Google Protocol Buffers that replaces the older gpb based exprotobuf with a protoc plugin, struct based messages and generated typespecs. The trade-off is that code generation moves into your build step and the gRPC plugin remains the only plugin supported.
Who is it for?
Adopt protobuf-elixir when your Elixir system already speaks Protocol Buffers and you want generated, typed message modules instead of runtime compiled Erlang records. Do not choose it if you need a plugin other than gRPC, if you want proto2 extensions as a stable contract, since that support is still marked experimental, or if you cannot add protoc and a code generation step to your build.
Can I use it commercially?
Yes. MIT is a permissive licence: you can use, modify and sell software built on it, as long as you keep its copyright and licence notices.
Is it still maintained?
Yes. The repository last received commits 30 days ago.
What is it written in?
Mainly Elixir, according to GitHub's language statistics.

Answers come from the project's GitHub data, last synced on November 7, 2026, and from our analysis. They are not legal advice.

From daily snapshots

Star history

  • elixir-protobuf/protobuf908 ★ · +2
Star history
Repository2026-10-212026-11-07
elixir-protobuf/protobuf906908

2 Stars in the last 17 days, measured from the daily GitHub snapshot. Compare with other projects

Editorial analysis

The problem it solves: protobufs in a language without a good toolchain

Protocol Buffers are the wire format that a lot of service-to-service traffic is written in, and the compiler that turns .proto files into code is called protoc. For a long time the Elixir options were thin. The library the project compares itself against, exprotobuf, was built on gpb, and the project lays out its own reasons for moving away from that route.

The list is specific. It ships a protoc plugin, so code generation works the same way it does for the official libraries in other languages. It generates simple and explicit code using Elixir's Macro facility, with a link to a generated example file for comparison. It supports plugins, though only grpc is supported at present. Messages are structs rather than Erlang records. And the generated code carries Typespecs.

The audience is Elixir teams that already have .proto files and either a gpb setup, a hand rolled encoder, or a service boundary they want to standardise. For a team that does not use Protocol Buffers at all, the whole category is overhead, and the feature list in the README makes that obvious: message definition DSL, decoding, unknown field skipping, embedded messages, packed and repeated fields, encoding, maps, default values, validation, typespecs and oneof.

One item carries a warning label: proto2 extension support is marked as an experiment and points at Protobuf.Extension in the documentation. That is a signal about how much of the protocol surface you can lean on today.

The mechanism: a protoc plugin that emits struct modules

The data flow is the standard protoc flow with an Elixir target. protoc parses your .proto file, hands the descriptor to the protoc-gen-elixir plugin, and the plugin writes a .pb.ex file into your source tree. The generated file contains one module per message, and each module uses the Protobuf behaviour with the syntax declared.

This is the core of the design. Nothing is compiled at runtime, and there is no Erlang term translation step. The generated module for a message declares its fields with a DSL, and the struct that results is a plain Elixir struct, so pattern matching and struct update syntax work as they do anywhere else in the language.

Encoding and decoding are two functions on the module. The documentation gives the shape:

elixir
struct = %Foo{a: 3.2, c: %Foo.Bar{}}
encoded = Foo.encode(struct)
struct = Foo.decode(encoded)

Validation happens during encoding, and the documentation is explicit that an error is raised when the struct misses a required field or holds a mistyped value. That is a design decision with consequences. A pipeline that treats encoding as a pure, always successful transformation has to change its error handling, because malformed state now fails loudly at the boundary instead of producing a message nobody can read.

Decoding is more forgiving in one specific direction. Unknown fields are skipped rather than rejected, which matters when a producer is ahead of the consumer in schema.

Installing the dependency and generating your first module

The package is added to the dependency list in mix.exs, and the version constraint in the documentation is:

elixir
def deps do
  [
    {:protobuf, "~> 0.16.0"}
  ]
end

Generation needs two extra pieces. The first is protoc itself, the protocol buffer compiler, which you download and install, and macOS users can install it through Homebrew. The second is the Elixir plugin binary, installed as a mix escript:

bash
    $ mix escript.install hex protobuf 0.16.0

The binary has to be on your PATH. The documentation gives two ways to achieve that: add PATH=~/.mix/escripts:$PATH to your bash or zsh profile, or, if you installed Elixir with asdf, run asdf reshim and then verify that protoc-gen-elixir works. It also warns that the escript version and the dependency version in your application must match, which is easy to get wrong when the version in the snippet above is older than the version you depend on.

With both pieces in place, generating code for a .proto file is one command:

bash
    $ protoc --elixir_out=./lib helloworld.proto

The result is a lib/helloworld.pb.ex file holding one module per message:

elixir
    defmodule Helloworld.HelloRequest do
      @moduledoc false
      use Protobuf, protoc_gen_elixir_version: "0.10.0", syntax: :proto3

      field :name, 1, type: :string
    end

    defmodule Helloworld.HelloReply do
      @moduledoc false
      use Protobuf, protoc_gen_elixir_version: "0.10.0", syntax: :proto3

      field :message, 1, type: :string
    end

Notice what the generated code does not contain: no records, no runtime table lookup, no decode specification to hand-maintain. The field number and the type are right there in the module.

protoc options that change the shape of the generated code

The plugin is configurable through options passed to protoc, and several of them change what ends up in your repository rather than just how it is compiled.

The first is descriptors. If your protos use custom options, the raw descriptors have to be included in the generated modules, and the descriptor/0 function on each module then exposes them:

bash
$ protoc --elixir_out=gen_descriptors=true:./lib/ *.proto
$ protoc --elixir_out=gen_descriptors=true,plugins=grpc:./lib/ *.proto

The second is source traceability. With gen_proto_source=true, every generated message and service module records the .proto file that defined it, and proto_source/0 returns that name as protoc knew it, for example "my/package/foo.proto". Without the option, proto_source/0 returns nil. The documentation notes a path caveat: the recorded name is relative to the import paths given with -I, and if you compile with absolute import paths then that path becomes part of the generated code, so the -I flags are worth reviewing before local filesystem details end up in a published artefact.

The third is naming and layout. package_prefix=my_app.protos prefixes every generated module, and one_file_per_module=true changes the output so that a MyPackage.MyMessage message ends up in my_package/my_message.pb.ex rather than sharing one file with other modules. include_docs=true controls whether the generated modules keep their documentation attribute, and transform_module=MyTransformModule adds a callback so you can supply your own encoding and decoding logic. Each of these is a protoc flag rather than a runtime setting, so the shape of your source tree is decided when you generate, not when you deploy.

The google_protos conflict that arrives with version 0.14.0

There is one upgrade hazard in the documentation, and it is worth reading twice if you are moving from an older release. Since version 0.14.0, the package includes all of the well known Google Protobuf modules. That collides with the separate, deprecated :google_protos package, and the two cannot live in the same dependency tree.

The fix is stated plainly: remove the :google_protos package from your dependencies and run the unlock command:

bash
mix deps.unlock --unused

The wider point is about how the project distributes the standard protocol definitions. Rather than asking you to add a second package for the well known types, the library folds them in. That removes a dependency but also removes a choice, and it means an application that today depends on :google_protos has to change its dependency list and recompile before the upgrade can succeed.

Version pinning matters here too. The escript that protoc calls and the :protobuf dependency in mix.exs are separate artefacts that are installed separately, and the documentation asks you to keep them on the same version. A mismatch between the plugin and the runtime library is the kind of problem that shows up as a confusing compile error in generated code rather than as a clear version warning.

What the repository layout says about the project

The top-level entries show a project with more infrastructure than a single library module. There is lib/ for the library code, src/ for the plugin sources, priv/ for what ships with the package, test/ for the test suite, and scripts/ for helper scripts. The build is described by mix.exs and mix.lock, and a .formatter.exs file at the root pins the formatting rules, which matters because the generated code has to satisfy the same formatter as the hand written code in the same repository.

Two directory names stand out. bench/ suggests the project measures something, and conformance/ suggests it checks itself against the protocol conformance suite. The names are suggestive, and the documentation does not describe what either directory contains or how to run them, so treat them as a signal about the project's intentions rather than as documented tooling you can invoke.

The README carries a CI badge and a coverage badge, both pointing at the main branch, and there is a coveralls.json in the root. The repository is not archived and the last push was on 2026-10-09, so the code is being worked on. What it does not have is a GitHub release, so there is no tag list to read and no published artefact to compare a dependency against. The CHANGELOG.md file is the only version history in the tree, and the version numbers you will meet in the documentation, 0.16.0 and 0.14.0 among them, are the ones to match against your own mix.lock.

Where protobuf-elixir is the wrong choice

Three boundaries are visible in the documentation itself.

The first is the plugin situation. The README states that plugins are supported and that only grpc is supported now. If your organisation generates code for several targets from the same.proto files, this project covers one of them and tells you plainly that the others are not there.

The second is proto2 extensions. They are listed among the features but marked as an experiment, with a pointer to Protobuf.Extension. Extensions are how legacy proto2 schemas extend messages, so if your service depends on them, you are relying on the least settled corner of the library.

The third is the build step itself. This is not a runtime library that reads a descriptor on boot. It is a compiler plugin plus a generated source tree, and the generated files then enter version control or a build artefact, which means your release process now has a code generation stage that must run protoc with the right options and keep the plugin version in step with the dependency version.

The alternative, exprotobuf with gpb, is described by the project itself as the thing it was built to replace, and the difference in approach is the whole point. exprotobuf, built on gpb, is the route the project positions itself against, with Erlang records instead of structs. protobuf-elixir gives you structs, typespecs and a normal Elixir module. That is the right trade when you want types, and the wrong one when a schema changes often enough that regenerating and committing code becomes the bottleneck.

Editorial conclusion

Adopt protobuf-elixir when your Elixir system already speaks Protocol Buffers and you want generated, typed message modules instead of runtime compiled Erlang records. Do not choose it if you need a plugin other than gRPC, if you want proto2 extensions as a stable contract, since that support is still marked experimental, or if you cannot add protoc and a code generation step to your build. Before you commit, run the escript install and the dependency on the same version, then decode one of your existing payloads to check field numbering and unknown field handling against your current implementation.

Frequently asked questions

How do I install protobuf-elixir and generate Elixir code from a .proto file?

Add {:protobuf, "~> 0.16.0"} to the dependencies in mix.exs, install the protoc compiler, then install the plugin with mix escript.install hex protobuf 0.16.0 and make sure protoc-gen-elixir is on your PATH. Running protoc --elixir_out=./lib helloworld.proto writes a lib/helloworld.pb.ex file.

How does protobuf-elixir differ from exprotobuf and gpb?

The project lists its reasons for moving away from exprotobuf and gpb: a protoc plugin, simple and explicit code generated with Macro, struct based messages instead of Erlang records, Typespecs in the generated code, and plugin support where only grpc is supported so far. Messages are structs rather than Erlang records.

What happens to the deprecated google_protos package?

Since version 0.14.0 the package includes all of the well known Google Protobuf modules, which conflicts with the deprecated :google_protos package. The documentation asks you to remove :google_protos from your dependencies and run mix deps.unlock --unused.

Does protobuf-elixir validate messages when encoding?

Yes. Validation is done during encoding, and an error is raised when the struct misses a required field or has a mistyped value. Decoding is more forgiving about fields it does not know, since unknown fields are skipped.

Can protobuf-elixir generate gRPC service code?

Yes, by passing plugins=grpc in --elixir_out. The documentation notes that gRPC is the only plugin supported at present.

Official sources

  1. elixir-protobuf/protobuf on GitHub
  2. Issues
  3. License: MIT
  4. Project website
  5. README
Add this badge to your README

If you maintain this project, the badge below links readers to this analysis and shows its maintenance status from the daily GitHub snapshot. Paste the markdown into your README; add ?metric=license or ?metric=stars to the image URL for a different field.

Add this badge to your README

markdown
[![Hysen Labs](https://hysenlabs.com/badge/elixir-protobuf-protobuf.svg)](https://hysenlabs.com/projects/elixir-protobuf-protobuf)

Star history chart for your README

markdown
[![Star history](https://hysenlabs.com/chart/elixir-protobuf-protobuf.svg)](https://hysenlabs.com/projects/elixir-protobuf-protobuf)

Was this useful?