gpb: a Google Protobuf compiler for Erlang
A Google Protobuf implementation for Erlang
At a glance
- What is it?
- gpb compiles Protocol Buffers definition files into Erlang modules and headers, mapping every protobuf type onto a native Erlang type, and offering records or maps depending on the options you pass. It is a mature tool with a quiet release history, and its licence is stated in the Makefile header rather than in the repository metadata.
- Who is it for?
- gpb fits an Erlang team that needs Protocol Buffers on the wire and wants generated code that uses Erlang's own data structures rather than a hand-rolled encoding layer. It is the wrong choice for a team that wants gRPC service stubs generated as well, since the README describes a compiler for definition files and a command line tool for generating modules and headers.
- Can I use it commercially?
- Check first. The repository uses a licence we do not classify automatically, so read its LICENSE file before any commercial use.
- Is it still maintained?
- Yes. The repository last received commits 149 days ago.
- What is it written in?
- Mainly Erlang, according to GitHub's language statistics.
Answers come from the project's GitHub data, last synced on October 23, 2026, and from our analysis. They are not legal advice.
Editorial analysis
A definition file becomes a module and a header
The README describes gpb as a compiler for Google protocol buffer definitions files for Erlang, and the first example shows the whole loop.
The input is a protobuf file, `x.proto`:
message Person {
required string name = 1;
required int32 id = 2;
optional string email = 3;
}The output is a pair of files, `x.erl` and `x.hrl`, which you then compile and use:
# erlc -I.../gpb/include x.erl
# erlThe `-I` pointing at the gpb include directory is the part to remember, because the generated code includes a header that ships with gpb itself, and that header is what carries the record definitions and the codec behaviour.
The README says the code for a definition can be generated in a number of different ways, and it shows the command line tool, `protoc-erl`, with a pointer to further down the document for integration with rebar. So there are two front ends to the same compiler, one for a shell and one for a build system, and the choice matters for how a project wires generation into its build.
The tool is published on hex.pm as `gpb`, with the API documentation on hexdocs.pm, and the primary language of the repository is Erlang.
Records by default, maps when you ask
The type mapping table is the heart of the library, and it is where the design decisions are visible.
The default is records. A protobuf message becomes a record, and thus a tuple, and a protobuf map becomes an unordered list of two-tuples, `[{Key,Value}]`. The `maps` option changes both: a message becomes a `map()` and a map field becomes a map as well, with the same option controlling the oneof representation.
The scalar mappings are mostly unsurprising, with the double and float types becoming `float() | infinity | '-infinity' | nan` and every integer type becoming `integer()`. The booleans become `true | false` and the enums become `atom()`.
Two of those have footnotes that matter in practice. Integers are accepted when encoding a float, and the integers one and zero are accepted when encoding a bool, so a hand-built value does not have to be converted first. And an unknown enum decodes to `integer()` rather than an atom, which is the behaviour you want when a peer sends a value your build has never heard of.
Strings are the one type with a real choice. The default is a unicode string, so a list of integers, and the `strings_as_binaries` option, written `-strbin` in the table, makes them binaries instead. The bytes type is a binary either way.
oneof and map fields need the newer protobuf syntax
The README is careful about the version history of the constructs it supports, and that is worth knowing before you port a schema.
The oneof construct first appeared in Google protobuf version 2.6.0, and it is represented in Erlang as a tuple of the chosen field name and its value, `{ChosenFieldName, Value}`. With the `{maps_oneof,flat}` option, written `-maps_oneof flat`, it is represented as `ChosenFieldName => Value` instead, which reads better in a map. A oneof field is always optional, so an unset oneof is implicitly `undefined` in the record form.
The map construct first appeared in Google protobuf version 3.0.0, for both the proto2 and the proto3 syntax, and it maps to the unordered list of two-tuples or, with the maps option, to a map. The README notes that for records the order of items is undefined when decoding, which is a small but real difference from the ordered behaviour you get in the maps form.
The two forms side by side, from the README, show the difference plainly:
#m4{f = []}
#m4{f = [{1, "a"}, {2, "b"}, {13, "hello"}]}So the choice of options is not a preference, it changes the data structures that every piece of your code will pattern match on. The `maps` option and the `strings_as_binaries` option are the two decisions to make before generating anything.
Optional fields are undefined, not absent
The handling of optional fields is where a generated-code library either matches the reference implementation or quietly diverges from it, so the README is specific.
Repeated fields are represented as lists. Optional fields are represented as either the value or `undefined` if not set.
For maps there is an extra layer. The `maps_unset_optional` option, when set to `omitted`, means unset optional values are omitted from the map instead of being set to `undefined` when encoding messages. When decoding messages, even with `maps_unset_optional` set to `omitted`, the default value will be set in the decoded map.
That asymmetry between encoding and decoding is worth internalising, because it is the sort of detail that produces a bug where a map round-trips differently in each direction. The README has a section on unset optionals and the `default` option for proto2 syntax, describing how decoding handles optional fields that are not present in the binary, and noting that the code generated by Google's protobuf compiler contains `has_<field>()` methods so you can examine them. That documentation is cut off in the visible text, so the full behaviour of the `default` option is not something the README here settles.
The shell session is where the generated code shows itself
The README walks through a full round trip in the Erlang shell, and the transcript is the clearest demonstration of what the generated code does.
Reading the record definitions is the first step, `rr("x.hrl")`, which loads the record definitions so the shell knows about them, and the result is a list of the available records. Then the encoder is called on a record value, and the output is a binary of the encoded message. Then a value is captured with `v(-1)`, which the README explains as referencing a value one step earlier in the history, and the same binary is decoded back into a record.
The point of the transcript is that the record syntax is the interface. You construct `#'Person'{...}` with the field names from the schema, hand it to `x:encode_msg/1`, and get back a binary, and `x:decode_msg/2` goes the other way. There is no builder, no option record, and no separate accessor API.
That design choice is what makes gpb pleasant in a language with records, and it is also its main constraint. The generated module is tied to the schema, so a schema change is a code change, and the module name comes from the file name, so two schemas with the same message name in different files need different file names.
The licence is in the Makefile, not the repository metadata
The licence story needs care, because the two sources disagree.
The repository listing records the licence as a custom one that GitHub cannot classify. But the Makefile at the root carries a full licence header, and it says this library is free software, that you can redistribute it and or modify it under the terms of the GNU Lesser General Public License as published by the Free Software Foundation, either version 2.1 of the License, or, at your option, any later version, with the warranty disclaimer and the standard instruction to accompany it with the licence text. The repository root carries `COPYING.LIB`, which is the file name GNU uses for the LGPL text.
So the header in the Makefile and the COPYING.LIB file both point at LGPL 2.1 or later, and the repository metadata simply has nothing it recognises. For a library that is about to be linked into your release, that difference between the metadata and the actual grant is worth resolving by reading the files rather than the badge.
The Makefile itself is also informative about the project. It is described as a makefile for gpb for projects that use GNU make to build, and it is dated 2013 with the copyright of Tomas Abrahamsson, who is also the author of the descriptor source and the helper scripts. The parameters it documents are the ones you would expect from a build integration, including `GPB_PREFIX` for including the makefile in a larger project, `ERL` and `ERLC` paths, `ERLC_FLAGS`, `EDOC_DEST_DIR` and `ERL_BATCH_FLAGS`.
A quiet 4.x to 5.0 transition in 2026
The release history is one of a long line of 4.x versions punctuated by a 5.0. The recent releases are 5.0.0 from 2026-05-27, 4.21.7 from 2026-02-06 and 4.21.6 from 2025-12-18. The repository is not archived, and the last push was on 2026-05-27, the same day as the 5.0.0 tag.
A major version from a compiler like this is worth pausing over, because it usually means the generated code changes shape. The options that decide whether a message is a record or a map are the kind of decision that gets baked into every call site, so a 5.0 release is a migration rather than a routine upgrade, and the README here does not describe what changed.
The repository layout shows the scale of the tool. There is `src/` for the compiler, `include/` for the headers that generated code depends on, `priv/` for runtime data, `bin/` for the command line entry points, `test/` for the test suite, `doc/` for the documentation sources, `descr_src/` for the descriptor definitions, `helpers/` for the helper scripts, `benchmarks/` for the benchmark harness, and `build/` for generated output. There is a `rebar.config.script` for the rebar3 build, an `.edts` project file, a `ChangeLog`, and a `README.nif-cc` that points at the erlang NIF toolchain.
A build badge points at a workflow named build on the master branch, and the project is published on hex.pm, which is where the versioned releases and the API documentation live.
Editorial conclusion
gpb fits an Erlang team that needs Protocol Buffers on the wire and wants generated code that uses Erlang's own data structures rather than a hand-rolled encoding layer. It is the wrong choice for a team that wants gRPC service stubs generated as well, since the README describes a compiler for definition files and a command line tool for generating modules and headers. Verify first which option set you want, since the maps and string options change the shape of every generated record, and check that the generated code compiles against your OTP version, because the release history shows the project moving from a 4.x line to 5.0.0 in 2026.
Frequently asked questions
What does gpb do with a .proto file?
gpb is a compiler for Google protocol buffer definition files for Erlang. The example compiles x.proto with the command line tool to produce x.erl and x.hrl, which you then compile with erlc, and the README also describes integration with rebar.
How does gpb map protobuf types onto Erlang types?
Messages become records, or map() with the maps option, strings become unicode strings or binary() with the strings_as_binaries option, enums become atom() with unknown enums decoding to integer(), and oneof fields become a {ChosenFieldName, Value} tuple, or a flat ChosenFieldName => Value with the {maps_oneof,flat} option.
How are optional fields represented in gpb-generated code?
Optional fields are represented as either the value or undefined if not set. For maps, the maps_unset_optional option set to omitted means unset optional values are omitted from the map when encoding, while decoding still sets the default value in the decoded map.
Which protobuf versions do the oneof and map constructs need?
The README states that the oneof construct first appeared in Google protobuf version 2.6.0, and the map construct first appeared in Google protobuf version 3.0.0 for both the proto2 and the proto3 syntax.
What licence applies to gpb?
The Makefile header states that the library is free software, redistributable and modifiable under the GNU Lesser General Public License version 2.1 or, at your option, any later version, and the repository carries a COPYING.LIB file, while the repository listing records the licence as one GitHub cannot classify.
Official sources
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.
[](https://hysenlabs.com/projects/tomas-abrahamsson-gpb)Was this useful?