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

Capabilities

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(number: Int) -> Decoder(Bool)

Decoder for boolean fields.

pub fn bool_with_default(
  number: Int,
  default: Bool,
) -> Decoder(Bool)

Decoder for boolean fields with a default value.

pub fn bytes(number: Int) -> Decoder(BitArray)

Decoder for bytes fields.

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 double(number: Int) -> Decoder(Float)

Decoder for double fields.

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 float(number: Int) -> Decoder(Float)

Decoder for float fields.

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(number: Int) -> Decoder(Int)

Decoder for int32 fields.

pub fn int32_with_default(
  number: Int,
  default: Int,
) -> Decoder(Int)

Decoder for int32 fields with a default value.

pub fn int64(number: Int) -> Decoder(Int)

Decoder for int64 fields.

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(number: Int) -> Decoder(String)

Decoder for string fields.

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(number: Int) -> Decoder(Int)

Decoder for uint32 fields.

pub fn uint32_with_default(
  number: Int,
  default: Int,
) -> Decoder(Int)

Decoder for uint32 fields with a default value.

pub fn uint64(number: Int) -> Decoder(Int)

Decoder for uint64 fields.

pub fn uint64_with_default(
  number: Int,
  default: Int,
) -> Decoder(Int)

Decoder for uint64 fields with a default value.

Search Document