跳到主要内容
版本:dev

Custom Serializers

Use a custom serializer when derive cannot express the type's serialized representation or when an opaque encoding is intentional. A serializer is a type-level implementation and names the value it handles through Target.

For a public third-party struct or enum whose schema should remain structural, prefer External-Type Serialization.

Implement a Custom Serializer

This example uses a local type as its own serializer:

use fory::{Error, Fory, ReadContext, Serializer, WriteContext};

#[derive(Debug, PartialEq)]
struct Point {
value: i32,
}

impl Serializer for Point {
type Target = Self;

fn write_data(value: &Self, context: &mut WriteContext) -> Result<(), Error> {
context.writer.write_i32(value.value);
Ok(())
}

fn read_data(context: &mut ReadContext) -> Result<Self, Error> {
Ok(Self {
value: context.reader.read_i32()?,
})
}

fn default_value(_context: &mut ReadContext) -> Result<Self, Error> {
Ok(Self { value: 0 })
}
}

let mut fory = Fory::builder().xlang(false).build();
fory.register_serializer::<Point>(100)?;

let value = Point { value: 42 };
let bytes = fory.serialize(&value)?;
let decoded: Point = fory.deserialize(&bytes)?;
assert_eq!(decoded, value);
# Ok::<(), Error>(())

write_data and read_data handle the EXT body. Fory's complete-value write and read operations supply the root or field reference and type-information framing.

default_value is optional. Implement it only when a null or missing compatible field has a meaningful value. It receives the active ReadContext, so a default that allocates can apply the same deserialization limits as a normal read.

Serialize a Third-Party Opaque Type

A separate serializer can target a type from another crate:

use fory::{Error, ReadContext, Serializer, WriteContext};

struct UuidSerializer;

#[cold]
#[inline(never)]
fn invalid_uuid(error: uuid::Error) -> Error {
Error::invalid_data(error.to_string())
}

impl Serializer for UuidSerializer {
type Target = uuid::Uuid;

fn write_data(
value: &uuid::Uuid,
context: &mut WriteContext,
) -> Result<(), Error> {
context.writer.write_bytes(value.as_bytes());
Ok(())
}

fn read_data(context: &mut ReadContext) -> Result<uuid::Uuid, Error> {
let bytes = context.reader.read_bytes(16)?;
uuid::Uuid::from_slice(bytes).map_err(invalid_uuid)
}
}

Register the serializer, then select it at the root or field:

fory.register_serializer::<UuidSerializer>(101)?;

let bytes = fory.serialize_with::<UuidSerializer>(&uuid)?;
let decoded =
fory.deserialize_with::<UuidSerializer>(&bytes)?;
#[derive(ForyStruct)]
struct Request {
#[fory(with = UuidSerializer)]
id: uuid::Uuid,
}

Custom serializer bodies are opaque. Compatible mode does not map fields inside them.

Support Arc<dyn Any + Send + Sync>

If a custom serializer's target must be materialized behind Arc<dyn Any + Send + Sync> or a synchronized application trait, implement read_arc_any:

use std::any::Any;
use std::sync::Arc;

impl Serializer for Point {
type Target = Self;

// Implement write_data, read_data, and any desired default as above.

fn read_arc_any(
context: &mut ReadContext,
) -> Result<Arc<dyn Any + Send + Sync>, Error> {
Ok(Arc::new(Self::read_data(context)?))
}
}

The target must implement Send + Sync. If this method is omitted, typed, Box, and Rc operations remain available, while synchronized Arc materialization returns an error.

Registration by Name

Use name registration when the serialized identity is a qualified name:

fory.register_serializer_by_name::<UuidSerializer>(
"example.Uuid",
)?;

One Fory instance can register at most one serializer for a target.

Context Access

WriteContext and ReadContext expose the binary writer and reader:

context.writer.write_i8(value);
context.writer.write_i32(value);
context.writer.write_var_u32(value);
context.writer.write_f64(value);

let value = context.reader.read_i8()?;
let value = context.reader.read_i32()?;
let value = context.reader.read_var_u32()?;
let value = context.reader.read_f64()?;

For variable-size bodies, validate readable bytes and graph-memory limits before allocating from an encoded length.

When a custom serializer is selected as a child of a variable-size carrier, the carrier must emit at least one aggregate byte per declared element or map entry after its count. Fory rejects serialization when the carrier's complete header, metadata, framing, and child bodies are shorter than that count, so it never emits bytes that the paired allocation-safety check cannot read. Fixed arrays are exempt because their validated count does not control an allocation; zero-sized elements in Vec, VecDeque, and BinaryHeap are also exempt because those carriers allocate no backing storage for them.