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

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

Info

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