---
title: Custom Serializers
sidebar_position: 6
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

  Unless required by applicable law or agreed to in writing, software
  distributed under the License is distributed on an "AS IS" BASIS,
  WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
  See the License for the specific language governing permissions and
  limitations under the License.
---

For types that don't support `FORY_STRUCT`, implement a `Serializer` template specialization manually.

## When to Use Custom Serializers

- External types from third-party libraries
- Types with special serialization requirements
- Legacy data format compatibility
- Performance-critical custom encoding
- Cross-language interoperability with custom protocols

## Implementing the Serializer Template

To create a custom serializer, specialize the `Serializer` template for your type within the `fory::serialization` namespace:

```cpp
#include "fory/serialization/fory.h"

using namespace fory::serialization;

// Define your custom type
struct MyExt {
  int32_t id;
  bool operator==(const MyExt &other) const { return id == other.id; }
};

namespace fory {
namespace serialization {

template <>
struct Serializer<MyExt> {
  // Declare as extension type for custom serialization
  static constexpr TypeId type_id = TypeId::EXT;

  // Main write method - handles null checking and type info
  static void write(const MyExt &value, WriteContext &ctx, RefMode ref_mode,
                    bool write_type, bool has_generics = false) {
    (void)has_generics;
    write_not_null_ref_flag(ctx, ref_mode);
    if (write_type) {
      auto result = ctx.write_any_type_info(
          static_cast<uint32_t>(TypeId::UNKNOWN),
          std::type_index(typeid(MyExt)));
      if (!result.ok()) {
        ctx.set_error(std::move(result).error());
        return;
      }
    }
    write_data(value, ctx);
  }

  // write only the data (no type info)
  static void write_data(const MyExt &value, WriteContext &ctx) {
    Serializer<int32_t>::write_data(value.id, ctx);
  }

  // write data with generics support
  static void write_data_generic(const MyExt &value, WriteContext &ctx,
                                 bool has_generics) {
    (void)has_generics;
    write_data(value, ctx);
  }

  // Main read method - handles null checking and type info
  static MyExt read(ReadContext &ctx, RefMode ref_mode, bool read_type) {
    bool has_value = read_null_only_flag(ctx, ref_mode);
    if (ctx.has_error() || !has_value) {
      return MyExt{};
    }
    if (read_type) {
      const TypeInfo *type_info = ctx.read_any_type_info(ctx.error());
      if (ctx.has_error()) {
        return MyExt{};
      }
      if (!type_info) {
        ctx.set_error(Error::type_error("TypeInfo for MyExt not found"));
        return MyExt{};
      }
    }
    return read_data(ctx);
  }

  // Read only the data (no type info)
  static MyExt read_data(ReadContext &ctx) {
    MyExt value;
    value.id = Serializer<int32_t>::read_data(ctx);
    return value;
  }

  // Read data with generics support
  static MyExt read_data_generic(ReadContext &ctx, bool has_generics) {
    (void)has_generics;
    return read_data(ctx);
  }

  // Read with pre-resolved type info
  static MyExt read_with_type_info(ReadContext &ctx, RefMode ref_mode,
                                   const TypeInfo &type_info) {
    (void)type_info;
    return read(ctx, ref_mode, false);
  }
};

} // namespace serialization
} // namespace fory
```

## Required Methods

A custom serializer must implement these static methods:

| Method                | Purpose                                         |
| --------------------- | ----------------------------------------------- |
| `write`               | Main serialization entry point with type info   |
| `write_data`          | Serialize data only (no type info)              |
| `write_data_generic`  | Serialize data with generics support            |
| `read`                | Main deserialization entry point with type info |
| `read_data`           | Deserialize data only (no type info)            |
| `read_data_generic`   | Deserialize data with generics support          |
| `read_with_type_info` | Deserialize with pre-resolved TypeInfo          |

The `type_id` constant should be set to `TypeId::EXT` for custom extension types.

## Registering Custom Serializers

Register your custom serializer with Fory before use:

```cpp
auto fory = Fory::builder().xlang(true).build();

// Register with numeric type ID (must match across languages)
auto result = fory.register_extension_type<MyExt>(103);
if (!result.ok()) {
  std::cerr << "Failed to register: " << result.error().to_string() << std::endl;
}

// Or register with type name for named type systems
fory.register_extension_type<MyExt>("my_ext");

// Or with namespace and type name
fory.register_extension_type<MyExt>("com.example", "MyExt");
```

## Complete Example

```cpp
#include "fory/serialization/fory.h"
#include <iostream>

using namespace fory::serialization;

struct CustomType {
  int32_t value;
  std::string name;

  bool operator==(const CustomType &other) const {
    return value == other.value && name == other.name;
  }
};

namespace fory {
namespace serialization {

template <>
struct Serializer<CustomType> {
  static constexpr TypeId type_id = TypeId::EXT;

  static void write(const CustomType &value, WriteContext &ctx,
                    RefMode ref_mode, bool write_type, bool has_generics = false) {
    (void)has_generics;
    write_not_null_ref_flag(ctx, ref_mode);
    if (write_type) {
      auto result = ctx.write_any_type_info(
          static_cast<uint32_t>(TypeId::UNKNOWN),
          std::type_index(typeid(CustomType)));
      if (!result.ok()) {
        ctx.set_error(std::move(result).error());
        return;
      }
    }
    write_data(value, ctx);
  }

  static void write_data(const CustomType &value, WriteContext &ctx) {
    // write value as varint for compact encoding
    Serializer<int32_t>::write_data(value.value, ctx);
    // Delegate string serialization to built-in serializer
    Serializer<std::string>::write_data(value.name, ctx);
  }

  static void write_data_generic(const CustomType &value, WriteContext &ctx,
                                 bool has_generics) {
    (void)has_generics;
    write_data(value, ctx);
  }

  static CustomType read(ReadContext &ctx, RefMode ref_mode, bool read_type) {
    bool has_value = read_null_only_flag(ctx, ref_mode);
    if (ctx.has_error() || !has_value) {
      return CustomType{};
    }
    if (read_type) {
      const TypeInfo *type_info = ctx.read_any_type_info(ctx.error());
      if (ctx.has_error()) {
        return CustomType{};
      }
      if (!type_info) {
        ctx.set_error(Error::type_error("TypeInfo for CustomType not found"));
        return CustomType{};
      }
    }
    return read_data(ctx);
  }

  static CustomType read_data(ReadContext &ctx) {
    CustomType value;
    value.value = Serializer<int32_t>::read_data(ctx);
    value.name = Serializer<std::string>::read_data(ctx);
    return value;
  }

  static CustomType read_data_generic(ReadContext &ctx, bool has_generics) {
    (void)has_generics;
    return read_data(ctx);
  }

  static CustomType read_with_type_info(ReadContext &ctx, RefMode ref_mode,
                                        const TypeInfo &type_info) {
    (void)type_info;
    return read(ctx, ref_mode, false);
  }
};

} // namespace serialization
} // namespace fory

int main() {
  auto fory = Fory::builder().xlang(true).build();
  fory.register_extension_type<CustomType>(100);

  CustomType original{42, "test"};

  auto serialized = fory.serialize(original);
  if (!serialized.ok()) {
    std::cerr << "Serialization failed" << std::endl;
    return 1;
  }

  auto deserialized = fory.deserialize<CustomType>(serialized.value());
  if (!deserialized.ok()) {
    std::cerr << "Deserialization failed" << std::endl;
    return 1;
  }

  assert(original == deserialized.value());
  std::cout << "Custom serializer works!" << std::endl;
  return 0;
}
```

## WriteContext Methods

The `WriteContext` provides methods for writing data:

```cpp
// Primitive types
ctx.write_uint8(value);
ctx.write_int8(value);
ctx.write_uint16(value);

// Variable-length integers (compact encoding)
ctx.write_var_uint32(value);   // Unsigned varint
ctx.write_varint32(value);    // Signed zigzag varint
ctx.write_var_uint64(value);   // Unsigned varint
ctx.write_varint64(value);    // Signed zigzag varint

// Tagged integers (for mixed-size encoding)
ctx.write_tagged_uint64(value);
ctx.write_tagged_int64(value);

// Raw bytes
ctx.write_bytes(data_ptr, length);

// Access underlying buffer for advanced operations
ctx.buffer().write_int32(value);
ctx.buffer().write_float(value);
ctx.buffer().write_double(value);
```

## ReadContext Methods

The `ReadContext` provides methods for reading data:

```cpp
// Primitive types (use error reference pattern)
uint8_t u8 = ctx.read_uint8(ctx.error());
int8_t i8 = ctx.read_int8(ctx.error());

// Variable-length integers
uint32_t u32 = ctx.read_var_uint32(ctx.error());
int32_t i32 = ctx.read_varint32(ctx.error());
uint64_t u64 = ctx.read_var_uint64(ctx.error());
int64_t i64 = ctx.read_varint64(ctx.error());

// Check for errors after read operations
if (ctx.has_error()) {
  return MyType{};  // Return default on error
}

// Access underlying buffer for advanced operations
int32_t value = ctx.buffer().read_int32(ctx.error());
float f = ctx.buffer().read_float(ctx.error());
double d = ctx.buffer().read_double(ctx.error());
```

## Delegating to Built-in Serializers

Reuse existing serializers for nested types:

```cpp
static void write_data(const MyType &value, WriteContext &ctx) {
  // Delegate to built-in serializers
  Serializer<int32_t>::write_data(value.int_field, ctx);
  Serializer<std::string>::write_data(value.string_field, ctx);
  Serializer<std::vector<int32_t>>::write_data(value.vec_field, ctx);
}

static MyType read_data(ReadContext &ctx) {
  MyType value;
  value.int_field = Serializer<int32_t>::read_data(ctx);
  value.string_field = Serializer<std::string>::read_data(ctx);
  value.vec_field = Serializer<std::vector<int32_t>>::read_data(ctx);
  return value;
}
```

## Best Practices

1. **Use variable-length encoding** for integers that may be small
2. **Check errors after read operations** using `ctx.has_error()`
3. **Return default values on error** to maintain consistent behavior
4. **Delegate to built-in serializers** for standard types
5. **Match type IDs across languages** for cross-language compatibility
6. **Use `(void)param`** to suppress unused parameter warnings

## Related Topics

- [Type Registration](type-registration.md) - Registering serializers
- [Basic Serialization](basic-serialization.md) - Using FORY_STRUCT macro
- [Schema Evolution](schema-evolution.md) - Compatible mode
- [Cross-Language](cross-language.md) - Cross-language serialization
