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
- Learn about importing .proto files
- Explore type extensions for custom serialization