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

Proto Editions

Protobuf editions are the successor to the proto2/proto3 split. Instead of picking a syntax for the whole file, an edition (identified by a year, such as 2023) defines a set of default behaviors, and individual features—like field presence—are configured per field. The goal is smoother, incremental evolution of the format while preserving backward compatibility.

nim-protobuf-serialization exposes editions through the {.proto.} pragma.

The {.proto.} Pragma

Annotate a message with {.proto.} instead of {.proto2.} or {.proto3.}:

# With {.proto.}, presence is chosen per field:
#   - PBExplicit: explicit presence (like proto2 optional)
#   - required:   must be present in the encoded message
#   - implicit:   no presence tracking (like proto3 scalars)
type
  Mixed {.proto.} = object
    a {.fieldNumber: 1, pint.}: PBExplicit[0'i32]
    b {.fieldNumber: 2, pint, required.}: int32
    c {.fieldNumber: 3, pint, implicit.}: int32

Unlike proto2 and proto3, {.proto.} lets you choose presence for each field:

  • PBExplicit—explicit presence: the field can distinguish "not set" from "set to the default value" (this is an alias of PBOption).
  • required—the field must be present in the encoded message.
  • implicit—no presence tracking: the field behaves like a proto3 scalar, where the zero value is indistinguishable from "not set".

Every field in a {.proto.} message must be exactly one of implicit, required, PBExplicit, or a repeated field (seq[T]).

Encoding and decoding work as usual:

let msg = Mixed(a: pbSome(1'i32), b: 7'i32, c: 3'i32)
let encoded = Protobuf.encode(msg)
let decoded = Protobuf.decode(encoded, Mixed)
assert decoded == msg

A required field that is missing from the encoded message is a decode error:

# A required field that is absent from the encoded message is a decode error.
block:
  var raised = false
  try:
    discard Protobuf.decode(default(seq[byte]), Mixed)
  except ProtobufReadError:
    raised = true
  assert raised

Implicit by Default

Adding implicit at the type level makes fields implicit by default, so they no longer need a per-field presence pragma (except required):

# {.proto, implicit.} makes fields implicit by default, so they no longer
# need a per-field presence pragma (except required).
type
  Settings {.proto, implicit.} = object
    host {.fieldNumber: 1.}: string
    port {.fieldNumber: 2, pint.}: int32
    token {.fieldNumber: 3, pint.}: PBExplicit[0'i32]

The Edition Year

The edition parameter selects the protobuf edition the type conforms to. It must be one of the supported editions—2023, 2024, or 2026—and bare {.proto.} defaults to 2023:

# The edition year selects the protobuf edition the type conforms to. It must
# be one of the supported editions (2023, 2024, 2026); bare {.proto.} defaults
# to 2023. The year is validated at compile time but does not currently change
# the wire format, so these three types serialize identically.
type
  Config2023 {.proto: 2023, implicit.} = object
    value {.fieldNumber: 1, pint.}: int32

  Config2024 {.proto: 2024, implicit.} = object
    value {.fieldNumber: 1, pint.}: int32

  Config2026 {.proto: 2026, implicit.} = object
    value {.fieldNumber: 1, pint.}: int32

Info

The edition year is validated at compile time (an unsupported year fails to compile), but it does not currently change the wire format. The behavioral differences between editions live in .proto file parsing, which is not yet supported. As a result, {.proto: 2023.}, {.proto: 2024.}, and {.proto: 2026.} serialize identically today:

# All three editions produce the same bytes.
let a = Protobuf.encode(Config2023(value: 5'i32))
let b = Protobuf.encode(Config2024(value: 5'i32))
let c = Protobuf.encode(Config2026(value: 5'i32))
assert a == b
assert b == c

Next Steps