Skip to main content
Version: 0.17

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.buffer for direct byte reads/writes in hot paths.
  • Register the serializer with the same identity (id or namespace + typeName) on every side.