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

Optional Fields

In protobuf, distinguishing between "field not set" and "field set to default value" can be important. This library provides two ways to handle optional fields: Opt[T] for proto3/proto2 and PBOption for proto2.

The Problem with Default Values

In proto3, fields have implicit default values:

# Problem: In proto3, you can't distinguish between "not set" and "set to default"
type
  MessageDefault {.proto3.} = object
    count {.fieldNumber: 1.}: int32  # Defaults to 0
    text {.fieldNumber: 2.}: string  # Defaults to ""

When you decode a message, you can't tell if count was explicitly set to 0 or just not set at all.

Using Opt[T] (Proto3 or Proto2)

For proto3, the recommended approach is to use Opt[T] from the results library (re-exported via protobuf_serialization/pkg/results). This wraps a value to make it explicitly optional:

# Solution: Use Opt[T] to make fields explicitly optional
type
  MessageOpt {.proto3.} = object
    count {.fieldNumber: 1, ext.}: Opt[int32]
    text {.fieldNumber: 2, ext.}: Opt[string]

Creating Optional Values

Use Opt.some() to create a value that is present, and Opt.none() to create a value that is absent:

# Creating optional values with Opt.some() and Opt.none()
let msg1 = MessageOpt(
  count: Opt.some(42'i32),
  text: Opt.some("hello")
)

let msg2 = MessageOpt(
  count: Opt.none(int32),
  text: Opt.none(string)
)

Checking if a Value is Present

Use isSome() and isNone() to check presence:

# Checking if a value is present
if msg1.count.isSome():
  echo "Count is set to: ", msg1.count.get()
else:
  echo "Count is not set"

Getting the Value

Use get() to retrieve the value:

# Getting the value
let count = msg1.count.get()  # Returns the int32 value
echo "Count: ", count

Use valueOr() to provide a default:

# Providing a default value
let count2 = msg2.count.valueOr(0'i32)  # Returns 0 if not set
echo "Count2: ", count2

Using PBOption (Proto2)

For proto2, use PBOption to make fields explicitly optional:

# For proto2, use PBOption to make fields explicitly optional
type
  Settings {.proto2.} = object
    username {.fieldNumber: 1, required.}: string
    theme {.fieldNumber: 2.}: PBOption[default(string)]
    fontSize {.fieldNumber: 3, pint.}: PBOption[0'i32]
    notifications {.fieldNumber: 4.}: PBOption[false]

Creating Optional Values

Use pbSome() to create a value that is present, and pbNone() to create a value that is absent:

# Creating optional values with pbSome() and pbNone()
let settings1 = Settings(
  username: "alice",
  theme: pbSome("dark"),
  fontSize: pbSome(14'i32),
  notifications: pbSome(true)
)

let settings2 = Settings(
  username: "bob",
  theme: pbSome("light"),
  fontSize: pbNone(0'i32),
  notifications: pbNone(false)
)

Encoding and Decoding

# Encoding and decoding
let encoded = Protobuf.encode(settings2)
let decoded = Protobuf.decode(encoded, Settings)

assert decoded.username == "bob"
assert decoded.theme.isSome
assert decoded.theme.get == "light"
assert decoded.fontSize.isNone
assert decoded.notifications.isNone

Getting the Value

Use get to retrieve the value, and valueOr to provide a default:

# Providing a default value
let fontSize = decoded.fontSize.valueOr(12'i32)
assert fontSize == 12
echo "Font size: ", fontSize

When to Use Each Approach

Use Opt[T] when:

  • Working with proto3
  • You need to distinguish "not set" from "set to default"
  • Building APIs where presence matters

Use PBOption when:

  • Working with proto2
  • You need to distinguish "not set" from "set to default"
  • You need to set a default value other than default(T)

Don't use either when:

  • Default values are acceptable
  • You don't need presence tracking
  • Performance is critical (adds overhead)

Next Steps