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 ofPBOption).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
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
- Learn about optional fields with
PBOption - Read the annotating objects tutorial for proto2 and proto3 basics