Enums
Protobuf enums map naturally to Nim enums. Support for enum fields is not part of the default import—it lives in a separate module that you have to import explicitly.
Importing std/enums
The core protobuf_serialization module does not know how to serialize enum
fields. To use them, import protobuf_serialization/std/enums:
import protobuf_serialization
import protobuf_serialization/std/enums
Without this import, the compiler has no serialization handlers for your enum type and the code will not compile.
Enum fields are annotated with the {.ext.}
pragma, because enum support is implemented as a type extension:
type
Status = enum
Unknown # ord 0
Active # ord 1
Banned # ord 2
Account {.proto3.} = object
status {.fieldNumber: 1, ext.}: Status
Encoding and decoding then works like any other field:
let account = Account(status: Active)
let encoded = Protobuf.encode(account)
let decoded = Protobuf.decode(encoded, Account)
assert decoded == account
The Zero-Value Requirement
In proto3, an enum must contain a constant that maps to 0. If it does not,
compilation fails with a {.fatal.} error. In the example above, Unknown
satisfies this requirement because it is the first value and therefore has
ordinal 0.
Closed Enum Semantics
Nim enums are closed: a value of an enum type can only be one of the
constants defined for that type. There is no way to hold a Status whose value
is 3 when only 0, 1, and 2 are defined.
Because protobuf enums are mapped onto Nim enums, they inherit this closed
behavior. This is not conformant with proto3's open-enum semantics, where an
unknown value is preserved so it can be re-serialized unchanged. In nim-protobuf-protobuf_serialization, unknown
values are not stored at all and therefore cannot be serialized back.
If you need proto3-conformant behavior, use an int32 field instead—see
Preserving proto3-Compatible Semantics
below.
Unknown Values
How an unknown value is handled on decode depends on the syntax:
proto3—the unknown value is dropped and the field keeps its zero value:
# Nim enums are closed: a decoded value that is not part of the enum cannot
# be represented, so it is dropped and the field keeps its zero value.
# The bytes "0803" encode field 1 (tag 08) with the value 3, which is not a
# valid Status. Decoding maps it to the zero value (Unknown) and drops the 3.
block:
let encoded = "0803".hexToSeqByte
let decoded = Protobuf.decode(encoded, Account)
assert decoded.status == Unknown
proto2 with required—the unknown value is a decode error:
type
RequiredAccount {.proto2.} = object
status {.fieldNumber: 1, required, ext.}: Status
# In proto2 with a required enum, an unknown value is a decode error.
block:
let encoded = "0803".hexToSeqByte
var raised = false
try:
discard Protobuf.decode(encoded, RequiredAccount)
except ProtobufReadError:
raised = true
assert raised
Enums With Holes
Enum ordinals do not have to be contiguous, and they may be negative:
# Enum ordinals do not have to be contiguous.
type
Priority = enum
Low = -10
Normal = 0
High = 10
Critical # ord 11
Task {.proto3.} = object
priority {.fieldNumber: 1, ext.}: Priority
Optional Enums
In proto2, wrap an enum field in a PBOption
to distinguish "not set" from "set to the default value". Use pbSome and
pbNone to construct the values:
type
OptionalAccount {.proto2.} = object
status {.fieldNumber: 1, ext.}: PBOption[default(Status)]
let present = OptionalAccount(status: pbSome(Active))
assert Protobuf.decode(Protobuf.encode(present), OptionalAccount) == present
# The PBOption parameter is the field's default value, not its type, so
# pbNone takes the default value too: default(Status), i.e. Unknown.
let absent = OptionalAccount(status: pbNone(default(Status)))
assert Protobuf.decode(Protobuf.encode(absent), OptionalAccount) == absent
The PBOption parameter is the field's default value, not its type. Writing
PBOption[default(Status)] stores a full Status (the value type is derived
from the default), and falls back to default(Status) (that is, Unknown) when
the field is not set. So this reads as "either a Status, or not set with a
default of Unknown". Passing the type directly (PBOption[Status]) does not
compile, because the parameter expects a value. pbNone likewise takes the
default value, matching the field declaration.
Repeated & Packed Enums
Repeated enum fields follow the same packing rules as other repeated numeric
fields (see Repeated & Packed Fields): proto2 defaults to
unpacked, proto3 defaults to packed, and the
{.packed.} pragma
overrides the default:
type
# proto2 repeated fields are unpacked by default.
StatusLogP2 {.proto2.} = object
statuses {.fieldNumber: 1, ext.}: seq[Status]
# proto3 repeated numeric/enum fields are packed by default.
StatusLogP3 {.proto3.} = object
statuses {.fieldNumber: 1, ext.}: seq[Status]
# The packed pragma overrides the default in either syntax.
StatusLogPacked {.proto2.} = object
statuses {.fieldNumber: 1, ext, packed: true.}: seq[Status]
Unknown entries in a repeated enum field are dropped; the valid entries are kept:
# Unknown entries in a repeated enum field are dropped; valid ones are kept.
# "080008030802" is three unpacked entries: 0, 3 (invalid), 2.
let encoded = "080008030802".hexToSeqByte
let decoded = Protobuf.decode(encoded, StatusLogP2)
assert decoded.statuses == @[Unknown, Banned]
Preserving proto3-Compatible Semantics
Because Nim enums are closed, they cannot preserve unknown values. If you need
the proto3-conformant behavior—where unknown values survive a decode/encode
round-trip—store the value as an int32 field with the
{.pint.} pragma instead
of a Nim enum. Named values become plain constants:
# To get proto3-conformant, open-enum behavior, store the value as an int32
# instead of a Nim enum. Named values become constants, and any unknown value
# is preserved on decode instead of being dropped.
const
StatusUnknown = 0'i32
StatusActive = 1'i32
StatusBanned = 2'i32
type
OpenAccount {.proto3.} = object
status {.fieldNumber: 1, pint.}: int32
Now an unknown value is preserved rather than dropped:
# The same "0803" bytes now round-trip: the unknown value 3 is preserved.
let encoded = "0803".hexToSeqByte
let decoded = Protobuf.decode(encoded, OpenAccount)
assert decoded.status == 3'i32
assert Protobuf.encode(decoded) == encoded
Compare this with the proto3 unknown-value behavior of a Nim
enum, where the value 3 was dropped and replaced with the zero value.
Next Steps
- Learn about repeated and packed fields for arrays
- Explore optional fields with
PBOption