Type Extensions
Type extensions allow you to define custom serialization logic for types that aren't directly annotated with protobuf pragmas. This is useful when you want to serialize external types or types with custom encoding requirements.
When to Use Type Extensions
Use type extensions when:
- You need to serialize types from external libraries
- You want custom encoding logic (e.g., compression, encryption)
- You're working with types that can't be annotated directly
- You need to serialize wrapper types
Basic Structure
A type extension consists of three procedures:
computeFieldSize—calculates the encoded sizewriteField—encodes the valuereadFieldInto—decodes the value
Simple Example
Let's create a custom type and make it serializable:
# Custom wrapper type (not annotated with proto2/proto3)
type
IntWrapper = object
value: int32
Now we define a message that uses this custom type:
# Message that uses the custom type
type
Container {.proto3.} = object
name {.fieldNumber: 1.}: string
data {.fieldNumber: 2, ext.}: IntWrapper
To make IntWrapper serializable, we need to implement the three required procedures:
# Define type extension for IntWrapper
Protobuf.extensionDefaults(IntWrapper, pint32, defaultSeq = true)
func computeFieldSize(
field: int,
value: IntWrapper,
ProtoType: type ProtobufExt,
skipDefault: static bool
): int =
computeFieldSize(field, value.value, pint32, skipDefault)
proc writeField(
stream: OutputStream,
field: int,
value: IntWrapper,
ProtoType: type ProtobufExt,
skipDefault: static bool = false
) {.raises: [IOError].} =
writeField(stream, field, value.value, pint32, skipDefault)
proc readFieldInto(
stream: InputStream,
value: var IntWrapper,
header: FieldHeader,
ProtoType: type ProtobufExt
): bool {.raises: [SerializationError, IOError].} =
readFieldInto(stream, value.value, header, pint32)
Using the Extension
Now you can use the custom type in your protobuf messages:
let container = Container(
name: "Test",
data: IntWrapper(value: 42)
)
let encoded = Protobuf.encode(container)
let decoded = Protobuf.decode(encoded, Container)
assert decoded.data.value == 42
Extension with Sequences
The extensionDefaults macro can generate default handlers for sequences:
import protobuf_serialization
type
CustomType = object
value: string
# Generate defaults for seq[CustomType]
Protobuf.extensionDefaults(CustomType, pstring, defaultSeq = true)
func computeFieldSize(
field: int,
value: CustomType,
ProtoType: type ProtobufExt,
skipDefault: static bool
): int =
computeFieldSize(field, value.value, pstring, skipDefault)
proc writeField(
stream: OutputStream,
field: int,
value: CustomType,
ProtoType: type ProtobufExt,
skipDefault: static bool = false
) {.raises: [IOError].} =
writeField(stream, field, value.value, pstring, skipDefault)
proc readFieldInto(
stream: InputStream,
value: var CustomType,
header: FieldHeader,
ProtoType: type ProtobufExt
): bool {.raises: [SerializationError, IOError].} =
readFieldInto(stream, value.value, header, pstring)
# Now you can use seq[CustomType]
type
Container {.proto3.} = object
items {.fieldNumber: 1, ext.}: seq[CustomType]
let container = Container(
items: @[
CustomType(value: "first"),
CustomType(value: "second"),
CustomType(value: "third")
]
)
let encoded = Protobuf.encode(container)
let decoded = Protobuf.decode(encoded, Container)
assert decoded.items.len == 3
assert decoded.items[0].value == "first"
assert decoded.items[1].value == "second"
assert decoded.items[2].value == "third"
echo "Extension with sequences example passed!"
Complete Example
Here's a complete example with a custom timestamp type:
import protobuf_serialization
# Custom timestamp type
type
Timestamp = object
seconds: int64
# Message using the custom type
type
Event {.proto3.} = object
name {.fieldNumber: 1.}: string
timestamp {.fieldNumber: 2, ext.}: Timestamp
# Type extension for Timestamp
Protobuf.extensionDefaults(Timestamp, pint64, defaultSeq = false)
func computeFieldSize(
field: int,
value: Timestamp,
ProtoType: type ProtobufExt,
skipDefault: static bool
): int =
computeFieldSize(field, value.seconds, pint64, skipDefault)
proc writeField(
stream: OutputStream,
field: int,
value: Timestamp,
ProtoType: type ProtobufExt,
skipDefault: static bool = false
) {.raises: [IOError].} =
writeField(stream, field, value.seconds, pint64, skipDefault)
proc readFieldInto(
stream: InputStream,
value: var Timestamp,
header: FieldHeader,
ProtoType: type ProtobufExt
): bool {.raises: [SerializationError, IOError].} =
readFieldInto(stream, value.seconds, header, pint64)
# Usage
let event = Event(
name: "UserLogin",
timestamp: Timestamp(seconds: 1234567890'i64)
)
let encoded = Protobuf.encode(event)
let decoded = Protobuf.decode(encoded, Event)
assert decoded.timestamp.seconds == 1234567890
echo "Complete type extension example passed!"
Key Points
- Mark fields using extensions with
{.ext.}pragma - The extended type itself should NOT be annotated with
{.proto2.}or{.proto3.} - Use
extensionDefaultsto generate default sequence handlers - Implement all three procedures:
computeFieldSize,writeField,readFieldInto - Extensions work with both single values and sequences
Next Steps
- Read the contributor's guide to learn how to contribute to the library