Keyboard shortcuts

Press or to navigate between chapters

Press S or / to search in the book

Press ? to show this help

Press Esc to hide this help

Annotating Objects

To make your Nim types protobuf-serializable, you need to annotate them with pragmas that define the protobuf schema.

Message Types

Every protobuf message must be annotated with either {.proto2.} or {.proto3.}:

type
  MyMessage {.proto3.} = object
    # fields here

This corresponds to the syntax declaration in .proto files:

syntax = "proto3";

message MyMessage {
  // fields here
}

Field Numbers

Every field must have a unique field number using the {.fieldNumber: N.} pragma:

type
  Person {.proto3.} = object
    name {.fieldNumber: 1.}: string
    age {.fieldNumber: 2, pint.}: int32
    email {.fieldNumber: 3.}: string

Field numbers are permanent—once assigned, they should never change for a given field.

Scalar Types

Protobuf supports several scalar types. Here's how they map to Nim types:

Protobuf TypeNim TypePragmaNotes
int32, int64int32, int64{.pint.}Varint-encoded (inefficient for negative values)
sint32, sint64int32, int64{.sint.}Zig-zag encoded (efficient for negative values)
uint32, uint64uint32, uint64{.pint.}Varint-encoded
fixed32, fixed64uint32, uint64{.fixed.}Fixed-width, always 4 or 8 bytes
sfixed32, sfixed64int32, int64{.fixed.}Fixed-width signed
floatfloat32-32-bit floating point
doublefloat64-64-bit floating point
boolbool-Boolean value
stringstring-UTF-8 encoded string
bytesseq[byte]-Arbitrary byte sequence

Integer Encoding Strategies

Choose the right encoding for your integers:

pint (p stands for "plain")—uses standard varint encoding. Best for positive values or small negative values

type
  Counter {.proto3.} = object
    count {.fieldNumber: 1, pint.}: int32

sint (s stands for "signed")—uses zig-zag encoding. Best for negative values

type
  Temperature {.proto3.} = object
    celsius {.fieldNumber: 1, sint.}: int32

fixed: Best for large values or when size predictability matters

type
  LargeNumber {.proto3.} = object
    value {.fieldNumber: 1, fixed.}: uint64

Nested Messages

You can use other protobuf messages as field types:

type
  Address {.proto3.} = object
    street {.fieldNumber: 1.}: string
    city {.fieldNumber: 2.}: string
    zipCode {.fieldNumber: 3.}: string

  PersonWithAddress {.proto3.} = object
    name {.fieldNumber: 1.}: string
    age {.fieldNumber: 2, pint.}: int32
    address {.fieldNumber: 3.}: Address

Proto2 vs Proto3 Differences

Required Fields (Proto2 only)

In proto2, every field must be explicitly marked as required, be a repeated field (seq), be a PBOption, or be an extension type:

1. required pragma — the field must be present in the encoded message:

# 1. required pragma
type
  Message1 {.proto2.} = object
    id {.fieldNumber: 1, required, pint.}: int32

2. Repeated field (seq) — repeated fields are always allowed without required:

# 2. Repeated field (seq)
type
  Message2 {.proto2.} = object
    tags {.fieldNumber: 1.}: seq[string]

3. PBOption — explicitly optional field that can be absent:

# 3. PBOption
type
  Message3 {.proto2.} = object
    name {.fieldNumber: 1.}: PBOption[default(string)]

4. Extension type (ext) — a custom type with user-defined serialization logic:

# 4. Extension type
type
  MyCustomType = object
    value: int32

Protobuf.extensionDefaults(MyCustomType, pint32)

func computeFieldSize(
    field: int,
    value: MyCustomType,
    ProtoType: type ProtobufExt,
    skipDefault: static bool
): int =
  computeFieldSize(field, value.value, pint32, skipDefault)

proc writeField(
    stream: OutputStream,
    field: int,
    value: MyCustomType,
    ProtoType: type ProtobufExt,
    skipDefault: static bool = false
) {.raises: [IOError].} =
  writeField(stream, field, value.value, pint32, skipDefault)

proc readFieldInto(
    stream: InputStream,
    value: var MyCustomType,
    header: FieldHeader,
    ProtoType: type ProtobufExt
): bool {.raises: [SerializationError, IOError].} =
  readFieldInto(stream, value.value, header, pint32)

type
  Message4 {.proto2.} = object
    data {.fieldNumber: 1, required, ext.}: MyCustomType

Complete Example

Here's a complete example with various field types:

import protobuf_serialization

type
  Person {.proto3.} = object
    name {.fieldNumber: 1.}: string
    age {.fieldNumber: 2, pint.}: int32
    email {.fieldNumber: 3.}: string

type
  Counter {.proto3.} = object
    count {.fieldNumber: 1, pint.}: int32

type
  Temperature {.proto3.} = object
    celsius {.fieldNumber: 1, sint.}: int32

type
  LargeNumber {.proto3.} = object
    value {.fieldNumber: 1, fixed.}: uint64

type
  Address {.proto3.} = object
    street {.fieldNumber: 1.}: string
    city {.fieldNumber: 2.}: string
    zipCode {.fieldNumber: 3.}: string

  PersonWithAddress {.proto3.} = object
    name {.fieldNumber: 1.}: string
    age {.fieldNumber: 2, pint.}: int32
    address {.fieldNumber: 3.}: Address

let person = Person(name: "Bob", age: 25, email: "bob@example.com")
let encoded = Protobuf.encode(person)
let decoded = Protobuf.decode(encoded, Person)
assert decoded == person

echo "Annotating objects example passed!"

Next Steps