Custom Serializers
A custom serializer lets you control exactly how a type is encoded and decoded. You only need one when:
- the type comes from a package you cannot modify and cannot be annotated with
@ForyStruct() - you need a completely custom binary layout
- you are implementing a union/discriminated type
For your own models, @ForyStruct() with code generation is almost always the better choice.
Implement Serializer<T>
Subclass Serializer<T> and implement write and read. Use context.buffer to read and write raw bytes:
import 'package:fory/fory.dart';
final class Person {
Person(this.name, this.age);
final String name;
final int age;
}
final class PersonSerializer extends Serializer<Person> {
const PersonSerializer();
@override
void write(WriteContext context, Person value) {
final buffer = context.buffer;
buffer.writeUtf8(value.name);
buffer.writeInt64(value.age);
}
@override
Person read(ReadContext context) {
final buffer = context.buffer;
return Person(buffer.readUtf8(), buffer.readInt64());
}
}
Register the serializer before you use it:
final fory = Fory();
fory.registerSerializer(
Person,
const PersonSerializer(),
namespace: 'example',
typeName: 'Person',
);
Writing Nested Objects
When your serializer has a field that is itself a Fory-managed type, use context.writeRef and context.readRef rather than calling fory.serialize recursively. This keeps reference tracking correct and avoids writing a full root frame inside a nested payload.
@override
void write(WriteContext context, Wrapper value) {
context.writeRef(value.child);
}
@override
Wrapper read(ReadContext context) {
return Wrapper(context.readRef() as Child);
}
If you do not need reference identity tracking for a nested value (i.e., you know the value will never appear more than once in a graph), use writeNonRef:
context.writeNonRef(value.child);
Unions
For a discriminated/tagged union, extend UnionSerializer<T> instead of Serializer<T>. Write a discriminant value first, then the active variant; read the discriminant and dispatch accordingly.
final class ShapeSerializer extends UnionSerializer<Shape> {
const ShapeSerializer();
@override
void write(WriteContext context, Shape value) {
// write active variant
}
@override
Shape read(ReadContext context) {
// read discriminant, return correct variant
throw UnimplementedError();
}
}
Circular References in Custom Serializers
If your serializer can encounter circular object graphs, bind the object to the reference tracker before reading its nested fields:
final value = Node.empty();
context.reference(value); // register the object first
value.next = context.readRef() as Node?; // now nested reads can refer back to it
return value;
Skipping this step causes back-references to that object to resolve to null.
Tips
- Use
context.bufferfor direct byte reads/writes in hot paths. - Register the serializer with the same identity (
idornamespace + typeName) on every side.