protozoa/decode
Protocol Buffer Decode Module
This module provides a composable, type-safe API for decoding Protocol Buffer binary data into Gleam values. It follows the gleam/dynamic/decode pattern for familiar composability and error handling, making it easy to work with protobuf data in Gleam applications.
Design Philosophy
- Composable decoders: Small, focused decoder functions that can be combined
- Type safety: Compile-time guarantees about decoded data structure
- Error handling: Clear error messages for debugging decode failures
- Performance: Efficient binary parsing with minimal allocations
- Gleam idioms: Follows Result and Option patterns familiar to Gleam developers
Capabilities
- All proto3 types: Scalars (int32, string, bool), messages, enums, repeated fields
- Advanced features: Oneofs, maps, optional fields, nested messages
- Wire format parsing: Handles protobuf binary wire format correctly
- Field-level decoding: Individual field decoders for fine-grained control
- Message-level decoding: Complete message decoders with field validation
- Default values: Proper proto3 default value handling
Usage Pattern
Generated code uses this module to create message-specific decoder functions:
pub fn decode_user(data: BitArray) -> Result(User, DecodeError) {
decode.message(data, user_decoder())
}
fn user_decoder() -> decode.Decoder(User) {
decode.into({
use name <- decode.field("name", decode.string_field(1))
use age <- decode.field("age", decode.int32_field(2))
User(name: name, age: age)
})
}
Error Handling
All decode functions return Result(T, DecodeError) with descriptive error messages
for debugging binary format issues, missing required fields, or type mismatches.
Types
Represents an error that occurred during decoding. Contains what was expected, what was found, and the path to the error.
pub type DecodeError {
DecodeError(
expected: String,
found: String,
path: List(String),
)
FieldNotFound(field_number: Int)
}
Constructors
-
DecodeError(expected: String, found: String, path: List(String)) -
FieldNotFound(field_number: Int)
A decoder is a function that takes a dict of fields and produces a value. This type allows for composable, type-safe decoding of Protocol Buffer messages. Fields are stored in a Dict for O(1) lookup performance. Returns either the decoded value or a list of all errors encountered.
pub opaque type Decoder(a)
Values
pub fn bool_with_default(
number: Int,
default: Bool,
) -> Decoder(Bool)
Decoder for boolean fields with a default value.
pub fn decode_zigzag(value: Int) -> Int
Decodes a zigzag-encoded integer back to its signed value. Zigzag encoding is used for sint32 and sint64 fields to efficiently encode negative numbers.
Examples
decode_zigzag(0) // Returns 0
decode_zigzag(1) // Returns -1
decode_zigzag(2) // Returns 1
pub fn fail(
expected: String,
found: String,
path: List(String),
) -> Decoder(a)
Create a decoder that always fails with an error message. Useful for handling unsupported fields or validation errors.
Examples
fail("Unsupported field type") // Always returns Error
pub fn field(
number: Int,
decoder: fn(@internal Field) -> Result(a, DecodeError),
) -> Decoder(a)
Decode a required field with a specific decoder. Returns an error if the field is not present.
Examples
field(1, varint_field) // Decodes field 1 as a varint
field(2, string_field) // Decodes field 2 as a string
pub fn field_with_default(
number: Int,
decoder: fn(@internal Field) -> Result(a, DecodeError),
default: a,
) -> Decoder(a)
Decode a field with a default value. Returns the decoded value if present, otherwise returns the default.
Examples
field_with_default(1, varint_field, 0) // Returns 0 if field 1 is missing
pub fn fixed32(number: Int) -> Decoder(Int)
Decoder for fixed32 fields (unsigned 32-bit integers).
pub fn fixed64(number: Int) -> Decoder(Int)
Decoder for fixed64 fields (unsigned 64-bit integers).
pub fn from_field_dict(
f: fn(dict.Dict(Int, List(@internal Field))) -> Result(
a,
List(DecodeError),
),
) -> Decoder(a)
Create a decoder from a function that takes a dict of fields. This is useful for creating custom decoders for complex types like oneofs.
Examples
from_field_dict(fn(fields) {
// Custom decoding logic using dict.get
Ok(value)
})
pub fn int32_with_default(
number: Int,
default: Int,
) -> Decoder(Int)
Decoder for int32 fields with a default value.
pub fn int64_with_default(
number: Int,
default: Int,
) -> Decoder(Int)
pub fn map(decoder: Decoder(a), f: fn(a) -> b) -> Decoder(b)
Transform the result of a decoder using a mapping function.
Examples
decode.int32(1)
|> decode.map(fn(x) { x * 2 })
pub fn nested_message(
number: Int,
decoder: Decoder(a),
) -> Decoder(a)
Decoder for nested message fields.
pub fn optional_field(
number: Int,
decoder: fn(@internal Field) -> Result(a, DecodeError),
) -> Decoder(Result(a, Nil))
Decode an optional field. Returns Ok(Ok(value)) if the field is present and valid, Ok(Error(Nil)) if the field is missing or invalid.
Examples
optional_field(1, varint_field) // Decodes optional field 1
pub fn optional_nested_message(
number: Int,
decoder: Decoder(a),
) -> Decoder(Result(a, Nil))
Decoder for optional nested message fields.
pub fn repeated_field(
number: Int,
decoder: fn(@internal Field) -> Result(a, DecodeError),
) -> Decoder(List(a))
Decode all fields with a given number (for repeated fields). Returns a list of all decoded values for the field number. Collects all errors encountered during decoding.
Examples
repeated_field(1, varint_field) // Decodes all field 1 occurrences
pub fn repeated_int32(number: Int) -> Decoder(List(Int))
Decoder for repeated int32 fields.
pub fn repeated_string(number: Int) -> Decoder(List(String))
Decoder for repeated string fields.
pub fn run(
data: BitArray,
with decoder: Decoder(a),
) -> Result(a, List(DecodeError))
Run a decoder on a BitArray containing Protocol Buffer data. Returns either the decoded value or a list of all errors encountered.
Examples
let decoder = field(1, varint_field)
run(<<8, 42>>, decoder) // Decodes field 1 with value 42
pub fn sfixed32(number: Int) -> Decoder(Int)
Decoder for sfixed32 fields (signed 32-bit integers).
pub fn sfixed64(number: Int) -> Decoder(Int)
Decoder for sfixed64 fields (signed 64-bit integers).
pub fn sint32(number: Int) -> Decoder(Int)
Decoder for sint32 fields (signed 32-bit integers with zigzag encoding).
Examples
sint32(1) // Decodes field 1 as a zigzag-encoded int32
pub fn sint64(number: Int) -> Decoder(Int)
Decoder for sint64 fields (signed 64-bit integers with zigzag encoding).
Examples
sint64(1) // Decodes field 1 as a zigzag-encoded int64
pub fn string_with_default(
number: Int,
default: String,
) -> Decoder(String)
Decoder for string fields with a default value.
pub fn success(value: a) -> Decoder(a)
Create a decoder that always succeeds with a value. Useful for providing default values or building complex decoders.
Examples
success(42) // Always returns Ok(42)
pub fn then(
decoder: Decoder(a),
next: fn(a) -> Decoder(b),
) -> Decoder(b)
Build a decoder using Gleam’s use syntax. This allows for composing multiple field decoders into a single decoder. Collects all errors from both decoder stages.
Examples
use name <- decode.then(decode.string(1))
use age <- decode.then(decode.int32(2))
decode.success(Person(name: name, age: age))
pub fn uint32_with_default(
number: Int,
default: Int,
) -> Decoder(Int)
Decoder for uint32 fields with a default value.
pub fn uint64_with_default(
number: Int,
default: Int,
) -> Decoder(Int)
Decoder for uint64 fields with a default value.