title: Custom Serializers sidebar_position: 11 id: custom-serializers license: | Licensed to the Apache Software Foundation (ASF) under one or more contributor license agreements. See the NOTICE file distributed with this work for additional information regarding copyright ownership. The ASF licenses this file to You under the Apache License, Version 2.0 (the “License”); you may not use this file except in compliance with the License. You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Custom serializers allow you to define exactly how a type is serialized and deserialized. This is useful for types that require special handling, optimization, or cross-language compatibility.
Custom serializers implement the ExtensionSerializer interface:
type ExtensionSerializer interface { // WriteData serializes the value to the buffer. // Only write the data - Fory handles type info and references. // Use ctx.Buffer() to access the ByteBuffer. // Use ctx.SetError() to report errors. WriteData(ctx *WriteContext, value reflect.Value) // ReadData deserializes the value from the buffer into the provided value. // Only read the data - Fory handles type info and references. // Use ctx.Buffer() to access the ByteBuffer. // Use ctx.SetError() to report errors. ReadData(ctx *ReadContext, value reflect.Value) }
Here's a simple custom serializer for a type with an integer field:
import ( "reflect" "github.com/apache/fory/go/fory" ) type MyExt struct { Id int32 } type MyExtSerializer struct{} func (s *MyExtSerializer) WriteData(ctx *fory.WriteContext, value reflect.Value) { myExt := value.Interface().(MyExt) ctx.Buffer().WriteVarint32(myExt.Id) } func (s *MyExtSerializer) ReadData(ctx *fory.ReadContext, value reflect.Value) { id := ctx.Buffer().ReadVarint32(ctx.Err()) value.Set(reflect.ValueOf(MyExt{Id: id})) } // Register the custom serializer f := fory.New(fory.WithXlang(true)) err := f.RegisterExtension(MyExt{}, 100, &MyExtSerializer{})
The WriteContext and ReadContext provide access to serialization resources:
| Method | Description |
|---|---|
Buffer() | Returns the *ByteBuffer for reading/writing |
Err() | Returns *Error for deferred error checking |
SetError(err) | Sets an error on the context |
HasError() | Returns true if an error has been set |
TypeResolver() | Returns the type resolver for nested types |
RefResolver() | Returns the reference resolver for ref support |
The ByteBuffer provides methods for reading and writing primitive types:
| Method | Description |
|---|---|
WriteBool(v bool) | Write a boolean |
WriteInt8(v int8) | Write a signed 8-bit integer |
WriteInt16(v int16) | Write a signed 16-bit integer |
WriteInt32(v int32) | Write a signed 32-bit integer |
WriteInt64(v int64) | Write a signed 64-bit integer |
WriteFloat32(v float32) | Write a 32-bit float |
WriteFloat64(v float64) | Write a 64-bit float |
WriteVarint32(v int32) | Write a variable-length signed 32-bit integer |
WriteVarint64(v int64) | Write a variable-length signed 64-bit integer |
WriteBinary(data []byte) | Write raw bytes |
All read methods take an *Error parameter for deferred error checking:
| Method | Description |
|---|---|
ReadBool(err *Error) bool | Read a boolean |
ReadInt8(err *Error) int8 | Read a signed 8-bit integer |
ReadInt16(err *Error) int16 | Read a signed 16-bit integer |
ReadInt32(err *Error) int32 | Read a signed 32-bit integer |
ReadInt64(err *Error) int64 | Read a signed 64-bit integer |
ReadFloat32(err *Error) float32 | Read a 32-bit float |
ReadFloat64(err *Error) float64 | Read a 64-bit float |
ReadVarint32(err *Error) int32 | Read a variable-length signed 32-bit integer |
ReadVarint64(err *Error) int64 | Read a variable-length signed 64-bit integer |
ReadBinary(length int, err *Error) []byte | Read raw bytes of specified length |
A custom serializer for a type with multiple fields:
type Point3D struct { X, Y, Z float64 Label string } type Point3DSerializer struct{} func (s *Point3DSerializer) WriteData(ctx *fory.WriteContext, value reflect.Value) { p := value.Interface().(Point3D) buf := ctx.Buffer() buf.WriteFloat64(p.X) buf.WriteFloat64(p.Y) buf.WriteFloat64(p.Z) // Write string as length + bytes labelBytes := []byte(p.Label) buf.WriteVarint32(int32(len(labelBytes))) buf.WriteBinary(labelBytes) } func (s *Point3DSerializer) ReadData(ctx *fory.ReadContext, value reflect.Value) { buf := ctx.Buffer() err := ctx.Err() x := buf.ReadFloat64(err) y := buf.ReadFloat64(err) z := buf.ReadFloat64(err) labelLen := buf.ReadVarint32(err) labelBytes := buf.ReadBinary(int(labelLen), err) value.Set(reflect.ValueOf(Point3D{ X: x, Y: y, Z: z, Label: string(labelBytes), })) } f := fory.New(fory.WithXlang(true)) f.RegisterExtension(Point3D{}, 101, &Point3DSerializer{})
When your type contains pointers, handle nil values explicitly:
type OptionalValue struct { Value *int64 } type OptionalValueSerializer struct{} func (s *OptionalValueSerializer) WriteData(ctx *fory.WriteContext, value reflect.Value) { ov := value.Interface().(OptionalValue) buf := ctx.Buffer() if ov.Value == nil { buf.WriteBool(false) // nil flag } else { buf.WriteBool(true) // not nil buf.WriteInt64(*ov.Value) } } func (s *OptionalValueSerializer) ReadData(ctx *fory.ReadContext, value reflect.Value) { buf := ctx.Buffer() err := ctx.Err() hasValue := buf.ReadBool(err) if !hasValue { value.Set(reflect.ValueOf(OptionalValue{Value: nil})) return } v := buf.ReadInt64(err) value.Set(reflect.ValueOf(OptionalValue{Value: &v})) }
Use ctx.SetError() to report errors:
func (s *MySerializer) ReadData(ctx *fory.ReadContext, value reflect.Value) { buf := ctx.Buffer() version := buf.ReadInt8(ctx.Err()) if ctx.HasError() { return } if version != 1 { ctx.SetError(fory.DeserializationErrorf("unsupported version: %d", version)) return } // Continue reading... value.Set(reflect.ValueOf(result)) }
More compact serialization, requires ID coordination across languages:
f.RegisterExtension(MyType{}, 100, &MySerializer{})
More flexible but more serialization cost, type name included in serialized data:
f.RegisterExtensionByName(MyType{}, "myapp.MyType", &MySerializer{})
WriteVarint32/WriteVarint64 for integers that are often smallRead(Write(value)) == valuectx.HasError() after reading to handle errors gracefullyfunc TestMySerializer(t *testing.T) { f := fory.New(fory.WithXlang(true)) f.RegisterExtension(MyType{}, 100, &MySerializer{}) original := MyType{Field: "test"} // Serialize data, err := f.Serialize(original) require.NoError(t, err) // Deserialize var result MyType err = f.Deserialize(data, &result) require.NoError(t, err) assert.Equal(t, original, result) }