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 Type | Nim Type | Pragma | Notes |
|---|---|---|---|
int32, int64 | int32, int64 | {.pint.} | Varint-encoded (inefficient for negative values) |
sint32, sint64 | int32, int64 | {.sint.} | Zig-zag encoded (efficient for negative values) |
uint32, uint64 | uint32, uint64 | {.pint.} | Varint-encoded |
fixed32, fixed64 | uint32, uint64 | {.fixed.} | Fixed-width, always 4 or 8 bytes |
sfixed32, sfixed64 | int32, int64 | {.fixed.} | Fixed-width signed |
float | float32 | - | 32-bit floating point |
double | float64 | - | 64-bit floating point |
bool | bool | - | Boolean value |
string | string | - | UTF-8 encoded string |
bytes | seq[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
- Learn about repeated and packed fields for arrays
- Explore oneof fields for union types