Skip to main content
Version: dev

Serialization

This page demonstrates common cross-language serialization patterns. Data serialized in one supported language can be deserialized in any other supported language when peers use matching type identity, field schema, and compatibility settings.

Remote Schema Metadata Limits

Compatible mode may receive remote metadata (TypeDef or TypeMeta) for types that are not already known by the reader. Fory limits how many distinct remote metadata versions can be accepted, and also limits the size of each received metadata body:

  • maxSchemaVersionsPerType: maximum accepted remote metadata versions for one logical type. The default is 10.
  • maxAverageSchemaVersionsPerType: average accepted remote metadata versions across all accepted remote types. The default is 3; the effective global floor is 8192 metadata entries.
  • maxTypeFields: maximum fields declared by one received struct metadata body. The default is 512.
  • maxTypeMetaBytes: maximum encoded metadata body bytes for one received TypeDef or TypeMeta body, excluding the 8-byte header and any extended-size varint. The default is 4096.

These limits are resource protections. They do not change wire format, registration requirements, dynamic type loading, unknown-type handling, or schema-evolution compatibility.

Raise these values only when the data is not malicious and a trusted peer sends larger metadata or many schema versions.

LanguageField-count optionMetadata-bytes optionPer-type optionAverage option
JavawithMaxTypeFieldswithMaxTypeMetaByteswithMaxSchemaVersionsPerTypewithMaxAverageSchemaVersionsPerType
ScalawithMaxTypeFieldswithMaxTypeMetaByteswithMaxSchemaVersionsPerTypewithMaxAverageSchemaVersionsPerType
KotlinwithMaxTypeFieldswithMaxTypeMetaByteswithMaxSchemaVersionsPerTypewithMaxAverageSchemaVersionsPerType
Pythonmax_type_fieldsmax_type_meta_bytesmax_schema_versions_per_typemax_average_schema_versions_per_type
JavaScript/TypeScriptmaxTypeFieldsmaxTypeMetaBytesmaxSchemaVersionsPerTypemaxAverageSchemaVersionsPerType
C++max_type_fieldsmax_type_meta_bytesmax_schema_versions_per_typemax_average_schema_versions_per_type
GoWithMaxTypeFieldsWithMaxTypeMetaBytesWithMaxSchemaVersionsPerTypeWithMaxAverageSchemaVersionsPerType
Rustmax_type_fieldsmax_type_meta_bytesmax_schema_versions_per_typemax_average_schema_versions_per_type
C#MaxTypeFieldsMaxTypeMetaBytesMaxSchemaVersionsPerTypeMaxAverageSchemaVersionsPerType
SwiftmaxTypeFieldsmaxTypeMetaBytesmaxSchemaVersionsPerTypemaxAverageSchemaVersionsPerType
DartmaxTypeFieldsmaxTypeMetaBytesmaxSchemaVersionsPerTypemaxAverageSchemaVersionsPerType

Serialize Built-in Types

Common types can be serialized automatically without registration: primitive numeric types, string, binary, array, list, map, and more.

Reduced-precision floating-point values are also part of the built-in xlang type system:

  • float16 and array<float16>
  • bfloat16 and array<bfloat16>

Use the language-specific carrier types documented in the type mapping reference. Python uses pyfory.Float16 and pyfory.BFloat16 as annotation markers only; scalar values are native Python float, and dense reduced-precision arrays use pyfory.Float16Array and pyfory.BFloat16Array. Go uses the float16 and bfloat16 packages for scalar, slice, and array carriers; JavaScript uses number for scalar float16 and bfloat16, and dense array carriers BoolArray, Float16Array, and BFloat16Array for the corresponding array<T> schemas. Dart uses double plus Float16Type or Bfloat16Type metadata for scalar fields, and Float16List / Bfloat16List for dense arrays. Java uses @ArrayType on supported reduced-precision carriers for array<float16> / array<bfloat16> schema, while general object arrays stay on the list path; C++, Rust, and C# provide their own dedicated scalar and array carriers.

When compatible=true, a direct struct/class field can evolve between list<T> and array<T> for dense bool/numeric T. Integer list element encodings in the same signedness and width domain match the corresponding dense array element domain. This applies only to the immediate matched field schema. It does not apply to nested collection, map, array, union, or generic positions. A peer list<T?> schema can be read into a local array<T> field when the actual payload has no null elements. If the payload carries a null element or ref-tracked element encoding, reading it into a local array<T> field raises a compatible-read error.

Java

import org.apache.fory.*;
import org.apache.fory.config.*;

import java.util.*;

public class Example1 {
public static void main(String[] args) {
Fory fory = Fory.builder().withXlang(true).build();
List<Object> list = ofArrayList(true, false, "str", -1.1, 1, new int[100], new double[20]);
byte[] bytes = fory.serialize(list);
// bytes can be deserialized by other languages
fory.deserialize(bytes);
Map<Object, Object> map = new HashMap<>();
map.put("k1", "v1");
map.put("k2", list);
map.put("k3", -1);
bytes = fory.serialize(map);
// bytes can be deserialized by other languages
fory.deserialize(bytes);
}
}

Python

import pyfory
import numpy as np

fory = pyfory.Fory(xlang=True)
object_list = [True, False, "str", -1.1, 1,
np.full(100, 0, dtype=np.int32), np.full(20, 0.0, dtype=np.double)]
data = fory.serialize(object_list)
# bytes can be deserialized by other languages
new_list = fory.deserialize(data)
object_map = {"k1": "v1", "k2": object_list, "k3": -1}
data = fory.serialize(object_map)
# bytes can be deserialized by other languages
new_map = fory.deserialize(data)
print(new_map)

Go

package main

import forygo "github.com/apache/fory/go/fory"
import "fmt"

func main() {
list := []any{true, false, "str", -1.1, 1, make([]int32, 10), make([]float64, 20)}
fory := forygo.NewFory(forygo.WithXlang(true))
bytes, err := fory.Marshal(list)
if err != nil {
panic(err)
}
var newValue any
// bytes can be deserialized by other languages
if err := fory.Unmarshal(bytes, &newValue); err != nil {
panic(err)
}
fmt.Println(newValue)
dict := map[string]any{
"k1": "v1",
"k2": list,
"k3": -1,
}
bytes, err = fory.Marshal(dict)
if err != nil {
panic(err)
}
// bytes can be deserialized by other languages
if err := fory.Unmarshal(bytes, &newValue); err != nil {
panic(err)
}
fmt.Println(newValue)
}

JavaScript

import Fory from "@apache-fory/core";

const fory = new Fory();
const input = fory.serialize("hello fory");
const result = fory.deserialize(input);
console.log(result);

Rust

use fory::Fory;

fn run() {
let fory = Fory::builder().xlang(true).build();
let bin = fory.serialize(&"hello".to_string()).expect("serialize success");
let obj: String = fory.deserialize(&bin).expect("deserialize success");
assert_eq!("hello".to_string(), obj);
}

Serialize Custom Types

User-defined types must be registered using the register API to establish the mapping relationship between types in different languages. Use consistent type names across all languages.

Java

import org.apache.fory.*;
import org.apache.fory.config.*;
import java.util.*;

public class Example2 {
public static class SomeClass1 {
Object f1;
Map<Byte, Integer> f2;
}

public static class SomeClass2 {
Object f1;
String f2;
List<Object> f3;
Map<Byte, Integer> f4;
Byte f5;
Short f6;
Integer f7;
Long f8;
Float f9;
Double f10;
short[] f11;
List<Short> f12;
}

public static Object createObject() {
SomeClass1 obj1 = new SomeClass1();
obj1.f1 = true;
obj1.f2 = ofHashMap((byte) -1, 2);
SomeClass2 obj = new SomeClass2();
obj.f1 = obj1;
obj.f2 = "abc";
obj.f3 = ofArrayList("abc", "abc");
obj.f4 = ofHashMap((byte) 1, 2);
obj.f5 = Byte.MAX_VALUE;
obj.f6 = Short.MAX_VALUE;
obj.f7 = Integer.MAX_VALUE;
obj.f8 = Long.MAX_VALUE;
obj.f9 = 1.0f / 2;
obj.f10 = 1 / 3.0;
obj.f11 = new short[]{(short) 1, (short) 2};
obj.f12 = ofArrayList((short) -1, (short) 4);
return obj;
}

// mvn exec:java -Dexec.mainClass="org.apache.fory.examples.Example2"
public static void main(String[] args) {
Fory fory = Fory.builder().withXlang(true).build();
fory.register(SomeClass1.class, "example.SomeClass1");
fory.register(SomeClass2.class, "example.SomeClass2");
byte[] bytes = fory.serialize(createObject());
// bytes can be deserialized by other languages
System.out.println(fory.deserialize(bytes));
}
}

Python

from dataclasses import dataclass
from typing import List, Dict, Any
import pyfory, array


@dataclass
class SomeClass1:
f1: Any
f2: Dict[pyfory.Int8, pyfory.Int32]


@dataclass
class SomeClass2:
f1: Any = None
f2: str = None
f3: List[str] = None
f4: Dict[pyfory.Int8, pyfory.Int32] = None
f5: pyfory.Int8 = None
f6: pyfory.Int16 = None
f7: pyfory.Int32 = None
# int type will be taken as `pyfory.Int64`.
# use `pyfory.Int32` for type hint if peer uses more narrow type.
f8: int = None
f9: pyfory.Float32 = None
# float type will be taken as `pyfory.Float64`
f10: float = None
f11: pyfory.Array[pyfory.Int16] = None
f12: List[pyfory.Int16] = None


if __name__ == "__main__":
f = pyfory.Fory(xlang=True)
f.register_type(SomeClass1, name="example.SomeClass1")
f.register_type(SomeClass2, name="example.SomeClass2")
obj1 = SomeClass1(f1=True, f2={-1: 2})
obj = SomeClass2(
f1=obj1,
f2="abc",
f3=["abc", "abc"],
f4={1: 2},
f5=2 ** 7 - 1,
f6=2 ** 15 - 1,
f7=2 ** 31 - 1,
f8=2 ** 63 - 1,
f9=1.0 / 2,
f10=1 / 3.0,
f11=array.array("h", [1, 2]),
f12=[-1, 4],
)
data = f.serialize(obj)
# bytes can be deserialized by other languages
print(f.deserialize(data))

Go

package main

import forygo "github.com/apache/fory/go/fory"
import "fmt"

func main() {
type SomeClass1 struct {
F1 any
F2 map[int8]int32
}

type SomeClass2 struct {
F1 any
F2 string
F3 []any
F4 map[int8]int32
F5 int8
F6 int16
F7 int32
F8 int64
F9 float32
F10 float64
F11 []int16
F12 []int16
}
serializer := forygo.NewFory(forygo.WithXlang(true))
if err := serializer.RegisterStructByName(SomeClass1{}, "example.SomeClass1"); err != nil {
panic(err)
}
if err := serializer.RegisterStructByName(SomeClass2{}, "example.SomeClass2"); err != nil {
panic(err)
}
obj1 := &SomeClass1{F1: true, F2: map[int8]int32{-1: 2}}
obj := &SomeClass2{
F1: obj1,
F2: "abc",
F3: []any{"abc", "abc"},
F4: map[int8]int32{1: 2},
F5: 127,
F6: 32767,
F7: 2147483647,
F8: 9223372036854775807,
F9: 1.0 / 2,
F10: 1.0 / 3.0,
F11: []int16{1, 2},
F12: []int16{-1, 4},
}
bytes, err := serializer.Marshal(obj)
if err != nil {
panic(err)
}
var newValue any
// bytes can be deserialized by other languages
if err := serializer.Unmarshal(bytes, &newValue); err != nil {
panic(err)
}
fmt.Println(newValue)
}

JavaScript

import Fory, { Type } from "@apache-fory/core";

// Describe data structures using JSON schema
const description = Type.struct(
{ typeName: "example.foo" },
{
foo: Type.string(),
},
);
const fory = new Fory();
const { serialize, deserialize } = fory.register(description);
const input = serialize({ foo: "hello fory" });
const result = deserialize(input);
console.log(result);

Rust

use chrono::{NaiveDate, NaiveDateTime};
use fory::{Fory, ForyStruct};
use std::collections::HashMap;

#[test]
fn complex_struct() {
#[derive(ForyStruct, Debug, PartialEq)]
struct Animal {
category: String,
}

#[derive(ForyStruct, Debug, PartialEq)]
struct Person {
c1: Vec<u8>, // binary
c2: Vec<i16>, // primitive array
animal: Vec<Animal>,
c3: Vec<Vec<u8>>,
name: String,
c4: HashMap<String, String>,
age: u16,
op: Option<String>,
op2: Option<String>,
date: NaiveDate,
time: NaiveDateTime,
c5: f32,
c6: f64,
}
let person: Person = Person {
c1: vec![1, 2, 3],
c2: vec![5, 6, 7],
c3: vec![vec![1, 2], vec![1, 3]],
animal: vec![Animal {
category: "Dog".to_string(),
}],
c4: HashMap::from([
("hello1".to_string(), "hello2".to_string()),
("hello2".to_string(), "hello3".to_string()),
]),
age: 12,
name: "helo".to_string(),
op: Some("option".to_string()),
op2: None,
date: NaiveDate::from_ymd_opt(2025, 12, 12).unwrap(),
time: NaiveDateTime::from_timestamp_opt(1689912359, 0).unwrap(),
c5: 2.0,
c6: 4.0,
};

let mut fory = Fory::builder().xlang(true).build();
fory
.register_by_name::<Animal>("example.foo2")
.expect("register Animal");
fory
.register_by_name::<Person>("example.foo")
.expect("register Person");
let bin = fory.serialize(&person).expect("serialize success");
let obj: Person = fory.deserialize(&bin).expect("deserialize success");
assert_eq!(person, obj);
}

Serialize Shared and Circular References

Shared references and circular references can be serialized automatically with no duplicate data or recursion errors. Enable reference tracking to use this feature.

Java

import org.apache.fory.*;
import org.apache.fory.config.*;
import java.util.*;

public class ReferenceExample {
public static class SomeClass {
SomeClass f1;
Map<String, String> f2;
Map<String, String> f3;
}

public static Object createObject() {
SomeClass obj = new SomeClass();
obj.f1 = obj;
obj.f2 = ofHashMap("k1", "v1", "k2", "v2");
obj.f3 = obj.f2;
return obj;
}

// mvn exec:java -Dexec.mainClass="org.apache.fory.examples.ReferenceExample"
public static void main(String[] args) {
Fory fory = Fory.builder()
.withXlang(true)
.withRefTracking(true)
.build();
fory.register(SomeClass.class, "example.SomeClass");
byte[] bytes = fory.serialize(createObject());
// bytes can be deserialized by other languages
System.out.println(fory.deserialize(bytes));
}
}

Python

from typing import Dict
import pyfory

class SomeClass:
f1: "SomeClass"
f2: Dict[str, str]
f3: Dict[str, str]

fory = pyfory.Fory(xlang=True, ref=True)
fory.register_type(SomeClass, name="example.SomeClass")
obj = SomeClass()
obj.f2 = {"k1": "v1", "k2": "v2"}
obj.f1, obj.f3 = obj, obj.f2
data = fory.serialize(obj)
# bytes can be deserialized by other languages
print(fory.deserialize(data))

Go

package main

import forygo "github.com/apache/fory/go/fory"
import "fmt"

func main() {
type SomeClass struct {
F1 *SomeClass
F2 map[string]string
F3 map[string]string
}
fory := forygo.NewFory(forygo.WithXlang(true), forygo.WithTrackRef(true))
if err := fory.RegisterStruct(SomeClass{}, 65); err != nil {
panic(err)
}
value := &SomeClass{F2: map[string]string{"k1": "v1", "k2": "v2"}}
value.F3 = value.F2
value.F1 = value
bytes, err := fory.Marshal(value)
if err != nil {
panic(err)
}
var newValue any
// bytes can be deserialized by other languages
if err := fory.Unmarshal(bytes, &newValue); err != nil {
panic(err)
}
fmt.Println(newValue)
}

JavaScript

import Fory, { Type } from "@apache-fory/core";

const description = Type.struct("example.foo", {
foo: Type.string(),
bar: Type.struct("example.foo").setTrackingRef(true),
});

const fory = new Fory({ ref: true });
const { serialize, deserialize } = fory.register(description);
const data: any = {
foo: "hello fory",
};
data.bar = data;
const input = serialize(data);
const result = deserialize(input);
console.log(result.bar.foo === result.foo);

Rust

Circular references cannot be implemented in Rust due to ownership restrictions.

See Also