| --- |
| title: Basic Serialization |
| sidebar_position: 1 |
| id: basic-serialization |
| 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. |
| --- |
| |
| This guide covers the core serialization APIs in the default xlang mode for Apache Fory JavaScript. |
| |
| ## Create a `Fory` Instance |
| |
| ```ts |
| import Fory from "@apache-fory/core"; |
| |
| const fory = new Fory(); |
| ``` |
| |
| Create one instance, register your schemas, and reuse it. Fory caches the generated serializers after the first `register` call, so recreating it on every request wastes that work. |
| |
| ## Define a Schema with `Type.struct` |
| |
| The most common path is to define a schema and register it. |
| |
| ```ts |
| import Fory, { Type } from "@apache-fory/core"; |
| |
| const accountType = Type.struct( |
| { typeName: "example.account" }, |
| { |
| id: Type.int64(), |
| owner: Type.string(), |
| active: Type.bool(), |
| nickname: Type.string().setNullable(true), |
| }, |
| ); |
| |
| const fory = new Fory(); |
| const { serialize, deserialize } = fory.register(accountType); |
| ``` |
| |
| ## Serialize and Deserialize |
| |
| ```ts |
| const bytes = serialize({ |
| id: 42n, |
| owner: "Alice", |
| active: true, |
| nickname: null, |
| }); |
| |
| const value = deserialize(bytes); |
| console.log(value); |
| // { id: 42n, owner: 'Alice', active: true, nickname: null } |
| ``` |
| |
| The returned `bytes` value is a `Uint8Array`/platform buffer and can be sent over the network or written to storage. |
| |
| ## Root-Level Dynamic Serialization |
| |
| `Fory` can also serialize dynamic root values without first binding a schema-specific serializer. |
| |
| ```ts |
| const fory = new Fory(); |
| |
| const bytes = fory.serialize( |
| new Map([ |
| ["name", "Alice"], |
| ["age", 30], |
| ]), |
| ); |
| |
| const value = fory.deserialize(bytes); |
| ``` |
| |
| This is convenient for dynamic payloads, but explicit schemas are usually better for stable interfaces and cross-language contracts. |
| |
| ## Primitive Values |
| |
| ```ts |
| const fory = new Fory(); |
| |
| fory.deserialize(fory.serialize(true)); |
| // true |
| |
| fory.deserialize(fory.serialize("hello")); |
| // 'hello' |
| |
| fory.deserialize(fory.serialize(123)); |
| // 123 |
| |
| fory.deserialize(fory.serialize(123n)); |
| // 123n |
| |
| fory.deserialize(fory.serialize(new Date("2021-10-20T09:13:00Z"))); |
| // Date |
| ``` |
| |
| ### Number and `bigint` |
| |
| JavaScript `number` is a 64-bit float, which cannot exactly represent all 64-bit integers. For cross-language contracts or anywhere exact integer sizes matter, use explicit field types in your schema: |
| |
| - `Type.int32()` — 32-bit integer; use JavaScript `number` |
| - `Type.int64()` — 64-bit integer; use JavaScript `bigint` |
| - `Type.float32()` / `Type.float64()` — floating-point |
| |
| Dynamic root serialization (calling `fory.serialize(someNumber)` without a schema) will infer a type, but the inferred type is not guaranteed by the API. Use a schema for any stable contract. |
| |
| ## Arrays, Maps, and Sets |
| |
| ```ts |
| const inventoryType = Type.struct("example.inventory", { |
| tags: Type.list(Type.string()), |
| counts: Type.map(Type.string(), Type.int32()), |
| labels: Type.set(Type.string()), |
| }); |
| |
| const fory = new Fory({ ref: true }); |
| const { serialize, deserialize } = fory.register(inventoryType); |
| |
| const bytes = serialize({ |
| tags: ["hot", "new"], |
| counts: new Map([ |
| ["apple", 3], |
| ["pear", 8], |
| ]), |
| labels: new Set(["featured", "seasonal"]), |
| }); |
| |
| const value = deserialize(bytes); |
| ``` |
| |
| ## Nested Structs |
| |
| ```ts |
| const addressType = Type.struct("example.address", { |
| city: Type.string(), |
| country: Type.string(), |
| }); |
| |
| const userType = Type.struct("example.user", { |
| name: Type.string(), |
| address: Type.struct("example.address", { |
| city: Type.string(), |
| country: Type.string(), |
| }), |
| }); |
| |
| const fory = new Fory(); |
| const { serialize, deserialize } = fory.register(userType); |
| |
| const bytes = serialize({ |
| name: "Alice", |
| address: { city: "Hangzhou", country: "CN" }, |
| }); |
| |
| const user = deserialize(bytes); |
| ``` |
| |
| If a nested value can be missing, mark it nullable: |
| |
| ```ts |
| const wrapperType = Type.struct("example.wrapper", { |
| child: Type.struct("example.child", { |
| name: Type.string(), |
| }).setNullable(true), |
| }); |
| ``` |
| |
| ## Decorator-Based Registration |
| |
| TypeScript decorators are also supported. |
| |
| ```ts |
| import Fory, { Type } from "@apache-fory/core"; |
| |
| @Type.struct("example.user") |
| class User { |
| @Type.int64() |
| id!: bigint; |
| |
| @Type.string() |
| name!: string; |
| } |
| |
| const fory = new Fory(); |
| const { serialize, deserialize } = fory.register(User); |
| |
| const user = new User(); |
| user.id = 1n; |
| user.name = "Alice"; |
| |
| const copy = deserialize(serialize(user)); |
| console.log(copy instanceof User); // true |
| ``` |
| |
| ## Nullability |
| |
| Field nullability is explicit in schema-based structs. |
| |
| ```ts |
| const nullableType = Type.struct("example.optional_user", { |
| name: Type.string(), |
| email: Type.string().setNullable(true), |
| }); |
| ``` |
| |
| If a field is not marked nullable and you try to write `null`, serialization throws. |
| |
| ## Debugging Generated Code |
| |
| You can inspect generated serializer code with `hooks.afterCodeGenerated`. |
| |
| ```ts |
| const fory = new Fory({ |
| hooks: { |
| afterCodeGenerated(code) { |
| console.log(code); |
| return code; |
| }, |
| }, |
| }); |
| ``` |
| |
| This is useful when debugging schema behavior, field ordering, or generated fast paths. |
| |
| ## Cross-Language Interoperability |
| |
| The default xlang format is shared by all Fory runtimes. The following sections cover its cross-language type mapping, type identity, and interoperability requirements. |
| |
| Fory JavaScript serializes to the same binary format as the Java, Python, C++, |
| Go, Rust, C#, Swift, Dart, Scala, and Kotlin Fory implementations. You can write a |
| message in JavaScript and read it in Java, or any other direction, without a |
| conversion layer. |
| |
| Things to keep in mind: |
| |
| - Fory JavaScript reads and writes cross-language payloads only; it does not support any native-mode format. |
| - JavaScript does not support out-of-band mode. |
| |
| ### Requirements for a Successful Round Trip |
| |
| For a message to survive a round trip between JavaScript and another language: |
| |
| 1. **Same type identity** on both sides — same numeric ID, or same `typeName`. |
| 2. **Compatible field types** — a `Type.int32()` field in JavaScript matches Java `int`, Go `int32`, C# `int`. |
| 3. **Same nullability** — if one side marks a field nullable, the other should too. |
| 4. Compatible schema evolution on both sides. JavaScript enables it by default. |
| 5. **Same reference tracking config** if your data has shared or circular references. |
| |
| ### Step-by-Step: JavaScript to Another Peer |
| |
| 1. Define the JavaScript schema with the same type name or numeric ID used by the peer. |
| 2. Register the schema in both peers. |
| 3. Match field types, nullability, and schema-evolution settings. |
| 4. Test a real payload end-to-end before shipping. |
| |
| JavaScript side: |
| |
| ```ts |
| import Fory, { Type } from "@apache-fory/core"; |
| |
| const messageType = Type.struct( |
| { typeName: "example.message" }, |
| { |
| id: Type.int64(), |
| content: Type.string(), |
| }, |
| ); |
| |
| const fory = new Fory(); |
| const { serialize } = fory.register(messageType); |
| |
| const bytes = serialize({ |
| id: 1n, |
| content: "hello from JavaScript", |
| }); |
| ``` |
| |
| On the other side, register the same `example.message` type (same name or same numeric ID) using the peer language's API: |
| |
| - [Java guide](../java/index.md) |
| - [Python guide](../python/index.md) |
| - [Go guide](../go/index.md) |
| - [Rust guide](../rust/index.md) |
| |
| ### Field Naming |
| |
| Fory matches fields by name. When models are defined in multiple languages, keep field names consistent — or at minimum use a naming scheme that maps unambiguously across languages (e.g. `snake_case` everywhere). |
| |
| With the default compatible schema evolution, field order differences are tolerated, but the names |
| themselves must still match. |
| |
| ### Numeric Types |
| |
| JavaScript `number` is a 64-bit float, which does not map cleanly to every integer type in other languages. Use explicit schema types: |
| |
| - `Type.int32()` for 32-bit integers (Java `int`, Go `int32`, C# `int`) |
| - `Type.int64()` with `bigint` values for 64-bit integers (Java `long`, Go `int64`) |
| - `Type.float32()` or `Type.float64()` for floating-point values |
| |
| ### Lists and Dense Arrays |
| |
| Use `Type.list(T)` for ordinary JavaScript `Array<T>` values and Fory |
| `list<T>` schema. Dense bool/numeric vectors use the explicit array builders |
| listed below. |
| |
| | Fory schema | JavaScript/TypeScript schema builder | |
| | ----------------- | ------------------------------------ | |
| | `list<int32>` | `Type.list(Type.int32())` | |
| | `array<bool>` | `Type.boolArray()` | |
| | `array<int8>` | `Type.int8Array()` | |
| | `array<int16>` | `Type.int16Array()` | |
| | `array<int32>` | `Type.int32Array()` | |
| | `array<int64>` | `Type.int64Array()` | |
| | `array<uint8>` | `Type.uint8Array()` | |
| | `array<uint16>` | `Type.uint16Array()` | |
| | `array<uint32>` | `Type.uint32Array()` | |
| | `array<uint64>` | `Type.uint64Array()` | |
| | `array<float16>` | `Type.float16Array()` | |
| | `array<bfloat16>` | `Type.bfloat16Array()` | |
| | `array<float32>` | `Type.float32Array()` | |
| | `array<float64>` | `Type.float64Array()` | |
| |
| ### Date and Time |
| |
| - `Type.timestamp()` — a point in time; round-trips as a JavaScript `Date` |
| - `Type.date()` — a date without time; deserializes as `Date` |
| - `Type.duration()` — exposed as a numeric millisecond value in JavaScript |
| |
| ### Polymorphic Fields |
| |
| `Type.any()` lets a field hold different concrete types, but it is harder to keep in sync across languages. Prefer explicit field schemas whenever possible. |
| |
| ```ts |
| const wrapperType = Type.struct( |
| { typeId: 3001 }, |
| { |
| payload: Type.any(), |
| }, |
| ); |
| ``` |
| |
| ### Enums |
| |
| Enum member **order** must match across languages. Fory encodes enums by ordinal position, not by value. |
| |
| ```ts |
| const Color = { Red: 1, Green: 2, Blue: 3 }; |
| const fory = new Fory(); |
| fory.register(Type.enum({ typeId: 210 }, Color)); |
| ``` |
| |
| Use the same type ID or type name in every peer. |
| |
| ### Safety Limits |
| |
| The `maxDepth` option bounds nested payloads. It does not change the binary format; it only controls what the local `Fory` instance accepts. |
| |
| ### Related Guides |
| |
| - [Supported Types](supported-types.md) |
| - [Schema Evolution](schema-evolution.md) |
| - [Xlang Serialization Specification](../../specification/xlang_serialization_spec.md) |
| |
| ### Built-in values |
| |
| ```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); |
| ``` |
| |
| ### Custom values |
| |
| ```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); |
| ``` |
| |
| ### Shared and circular references |
| |
| ```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); |
| ``` |
| |
| ## Related Topics |
| |
| - [Type Registration](type-registration.md) |
| - [Supported Types](supported-types.md) |
| - [References](references.md) |