title: Custom Serializers sidebar_position: 5 id: dart_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
A custom serializer lets you control exactly how a type is encoded and decoded. You only need one when:
@ForyStruct()For your own models, @ForyStruct() with code generation is almost always the better choice.
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.writeInt64FromInt(value.age); } @override Person read(ReadContext context) { final buffer = context.buffer; return Person(buffer.readUtf8(), buffer.readInt64AsInt()); } }
Register the serializer before you use it:
final fory = Fory(); fory.registerSerializer( Person, const PersonSerializer(), namespace: 'example', typeName: 'Person', );
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);
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(); } }
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.
context.buffer for direct byte reads/writes in hot paths.id or namespace + typeName) on every side.