Skip to main content
Version: dev

Schema IDL

This document provides the syntax and semantic reference for Fory IDL.

For compiler usage and build integration, see Compiler Guide. For protobuf/FlatBuffers frontend mapping rules, see Protocol Buffers IDL Support and FlatBuffers IDL Support.

File Structure

An Fory IDL file typically consists of:

  1. Optional package declaration
  2. Optional file-level options
  3. Optional import statements
  4. Type definitions (enums, messages, and unions)
// Optional package declaration
package com.example.models;

// Optional file-level options
option java_package = "com.example.models";

// Import statements
import "common/types.fdl";

// Type definitions
enum Color [id=100] { ... }
message User [id=101] { ... }
message Order [id=102] { ... }
union Event [id=103] { ... }

Comments

Fory IDL supports both single-line and block comments:

// This is a single-line comment

/*
* This is a block comment
* that spans multiple lines
*/

message Example {
string name = 1; // Inline comment
}

Package Declaration

The package declaration defines the namespace for all types in the file.

package com.example.models;

You can optionally specify a package alias used for auto-generated type IDs:

package com.example.models alias models_v1;

Rules:

  • Optional but recommended
  • Must appear before any type definitions
  • Only one package declaration per file
  • Used for namespace-based type registration
  • Package alias is used for auto-ID hashing

Language Mapping:

LanguagePackage Usage
JavaJava package
PythonModule name (dots to underscores)
GoPackage name (last component)
RustModule name (dots to underscores)
C++Namespace (dots to ::)
C#Namespace
JavaScriptModule name (last segment)
DartLibrary name (package segments)

File-Level Options

Options can be specified at file level to control language-specific code generation.

Syntax

option option_name = value;

Java Package Option

Override the Java package for generated code:

package payment;
option java_package = "com.mycorp.payment.v1";

message Payment {
string id = 1;
}

Effect:

  • Generated Java files will be in com/mycorp/payment/v1/ directory
  • Java package declaration will be package com.mycorp.payment.v1;
  • Type registration still uses the Fory IDL package (payment) for cross-language compatibility

Go Package Option

Specify the Go import path and package name:

package payment;
option go_package = "github.com/mycorp/apis/gen/payment/v1;paymentv1";

message Payment {
string id = 1;
}

Format: "import/path;package_name" or just "import/path" (last segment used as package name)

Effect:

  • Generated Go files will have package paymentv1
  • The import path can be used in other Go code
  • Type registration still uses the Fory IDL package (payment) for cross-language compatibility

C# Namespace Option

Override the C# namespace for generated code:

package payment;
option csharp_namespace = "MyCorp.Payment.V1";

message Payment {
string id = 1;
}

Effect:

  • Generated C# files use namespace MyCorp.Payment.V1;
  • Output path follows namespace segments (MyCorp/Payment/V1/ under --csharp_out)
  • Type registration still uses the Fory IDL package (payment) for cross-language compatibility

Go Nested Type Style Option

Control Go naming for nested message/enum/union types:

package payment;
option go_nested_type_style = "camelcase";

message Envelope {
message Payload {
string id = 1;
}
}

Values:

  • underscore (default): Envelope_Payload
  • camelcase: EnvelopePayload

The CLI flag --go_nested_type_style overrides this schema option when both are set.

Swift Namespace Style Option

Control how package namespace is reflected in Swift generated type names:

package payment.v1;
option swift_namespace_style = "flatten";

message Payment {
string id = 1;
}

Values:

  • enum (default): namespace wrappers (for example Payment.V1.Payment)
  • flatten: package prefix on top-level types (for example Payment_V1_Payment)

Important: namespace wrapper/prefixing is only applied when package is non-empty. If package is empty, Swift emits top-level types directly for both styles.

The CLI flag --swift_namespace_style overrides this schema option when both are set.

Java Outer Classname Option

Generate all types as inner classes of a single outer wrapper class:

package payment;
option java_outer_classname = "DescriptorProtos";

enum Status {
UNKNOWN = 0;
ACTIVE = 1;
}

message Payment {
string id = 1;
Status status = 2;
}

Effect:

  • Generates a single file DescriptorProtos.java instead of separate files
  • All enums and messages become public static inner classes
  • The outer class is public final with a private constructor
  • Useful for grouping related types together

Generated structure:

public final class DescriptorProtos {
private DescriptorProtos() {}

public static enum Status {
UNKNOWN,
ACTIVE;
}

public static class Payment {
private String id;
private Status status;
// ...
}
}

Combined with java_package:

package payment;
option java_package = "com.example.proto";
option java_outer_classname = "PaymentProtos";

message Payment {
string id = 1;
}

This generates com/example/proto/PaymentProtos.java with all types as inner classes.

Java Multiple Files Option

Control whether types are generated in separate files or as inner classes:

package payment;
option java_outer_classname = "PaymentProtos";
option java_multiple_files = true;

message Payment {
string id = 1;
}

message Receipt {
string id = 1;
}

Behavior:

java_outer_classnamejava_multiple_filesResult
Not setAnySeparate files (one per type)
Setfalse (default)Single file with all types as inner classes
SettrueSeparate files (overrides outer class)

Effect of java_multiple_files = true:

  • Each top-level enum and message gets its own .java file
  • Overrides java_outer_classname behavior
  • Useful when you want separate files but still specify an outer class name for other purposes

Example without java_multiple_files (default):

option java_outer_classname = "PaymentProtos";
// Generates: PaymentProtos.java containing Payment and Receipt as inner classes

Example with java_multiple_files = true:

option java_outer_classname = "PaymentProtos";
option java_multiple_files = true;
// Generates: Payment.java, Receipt.java (separate files)

Multiple Options

Multiple options can be specified:

package payment;
option java_package = "com.mycorp.payment.v1";
option go_package = "github.com/mycorp/apis/gen/payment/v1;paymentv1";
option deprecated = true;

message Payment {
string id = 1;
}

Protobuf Extension Syntax

In .fdl files, use native Fory IDL syntax only (for example, [id=100], ref, optional, nullable=true).

Protobuf extension syntax with (fory). is for .proto files and the protobuf frontend only.

For protobuf extension options, see Protocol Buffers IDL Support.

Option Priority

For language-specific packages/namespaces:

  1. Command-line package override (highest priority)
  2. Language-specific option (java_package, go_package, csharp_namespace)
  3. Fory IDL package declaration (fallback)

Example:

package myapp.models;
option java_package = "com.example.generated";
ScenarioJava Package Used
No overridecom.example.generated
CLI: --package=overrideoverride
No java_package optionmyapp.models (fallback)

Cross-Language Type Registration

Language-specific options only affect where code is generated, not the type namespace used for serialization. This ensures cross-language compatibility:

package myapp.models;
option java_package = "com.mycorp.generated";
option go_package = "github.com/mycorp/gen;genmodels";

message User {
string name = 1;
}

All languages will register User with namespace myapp.models, enabling:

  • Java serialized data → Go deserialization
  • Go serialized data → Java deserialization
  • Any language combination works seamlessly

Import Statement

Import statements allow you to use types defined in other Fory IDL files.

Basic Syntax

import "path/to/file.fdl";

Multiple Imports

import "common/types.fdl";
import "common/enums.fdl";
import "models/address.fdl";

Path Resolution

Import paths are resolved relative to the importing file:

project/
├── common/
│ └── types.fdl
├── models/
│ ├── user.fdl # import "../common/types.fdl"
│ └── order.fdl # import "../common/types.fdl"
└── main.fdl # import "common/types.fdl"

Rules:

  • Import paths are quoted strings (double or single quotes)
  • Paths are resolved relative to the importing file's directory
  • Imported types become available as if defined in the current file
  • Circular imports are detected and reported as errors
  • Transitive imports work (if A imports B and B imports C, A has access to C's types)

Complete Example

common/types.fdl:

package common;

enum Status [id=100] {
PENDING = 0;
ACTIVE = 1;
COMPLETED = 2;
}

message Address [id=101] {
string street = 1;
string city = 2;
string country = 3;
}

models/user.fdl:

package models;
import "../common/types.fdl";

message User [id=200] {
string id = 1;
string name = 2;
Address home_address = 3; // Uses imported type
Status status = 4; // Uses imported enum
}

Unsupported Import Syntax

The following protobuf import modifiers are not supported:

// NOT SUPPORTED - will produce an error
import public "other.fdl";
import weak "other.fdl";

import public: Fory IDL uses a simpler import model. All imported types are available to the importing file only. Re-exporting is not supported. Import each file directly where needed.

import weak: Fory IDL requires all imports to be present at compile time. Optional dependencies are not supported.

Import Errors

The compiler reports errors for:

  • File not found: The imported file doesn't exist
  • Circular import: A imports B which imports A (directly or indirectly)
  • Parse errors: Syntax errors in imported files
  • Unsupported syntax: import public or import weak

Enum Definition

Enums define a set of named integer constants.

Basic Syntax

enum Status {
PENDING = 0;
ACTIVE = 1;
COMPLETED = 2;
}

With Explicit Type ID

enum Status [id=100] {
PENDING = 0;
ACTIVE = 1;
COMPLETED = 2;
}

Reserved Values

Reserve field numbers or names to prevent reuse:

enum Status {
reserved 2, 15, 9 to 11, 40 to max; // Reserved numbers
reserved "OLD_STATUS", "DEPRECATED"; // Reserved names
PENDING = 0;
ACTIVE = 1;
COMPLETED = 3;
}

Enum Type Options

Enum-level options are declared inline in [] after the enum name:

enum Status [deprecated=true] {
PENDING = 0;
ACTIVE = 1;
}

FDL does not support option ...; statements inside enum bodies.

Unsupported:

  • allow_alias is not supported. Each enum value must have a unique integer.

Language Mapping

LanguageImplementation
Javaenum Status { UNKNOWN, ACTIVE, ... }
Pythonclass Status(IntEnum): UNKNOWN = 0
Gotype Status int32 with constants
Rust#[repr(i32)] enum Status { Unknown }
C++enum class Status : int32_t { ... }
JavaScriptexport enum Status { UNKNOWN, ... }
Dartenum Status { unknown, active, ... }

Enum Prefix Stripping

When enum values use a protobuf-style prefix (enum name in UPPER_SNAKE_CASE), the compiler automatically strips the prefix for languages with scoped enums:

// Input with prefix
enum DeviceTier {
DEVICE_TIER_UNKNOWN = 0;
DEVICE_TIER_TIER1 = 1;
DEVICE_TIER_TIER2 = 2;
}

Generated code:

LanguageOutputStyle
JavaUNKNOWN, TIER1, TIER2Scoped enum
RustUnknown, Tier1, Tier2Scoped enum
C++UNKNOWN, TIER1, TIER2Scoped enum
PythonUNKNOWN, TIER1, TIER2Scoped IntEnum
GoDeviceTierUnknown, DeviceTierTier1, ...Unscoped const
JavaScriptUNKNOWN, TIER1, TIER2Scoped enum
Dartunknown, tier1, tier2Scoped enum

Note: The prefix is only stripped if the remainder is a valid identifier. For example, DEVICE_TIER_1 is kept unchanged because 1 is not a valid identifier name.

Grammar:

enum_def     := 'enum' IDENTIFIER [type_options] '{' enum_body '}'
type_options := '[' type_option (',' type_option)* ']'
type_option := IDENTIFIER '=' option_value
enum_body := (reserved_stmt | enum_value)*
reserved_stmt := 'reserved' reserved_items ';'
enum_value := IDENTIFIER '=' INTEGER ';'

Rules:

  • Enum names must be unique within the file
  • Enum values must have explicit integer assignments
  • Value integers must be unique within the enum (no aliases)
  • Type ID ([id=100]) is optional for enums but recommended for cross-language use

Example with All Features:

// HTTP status code categories
enum HttpCategory [id=200] {
reserved 10 to 20; // Reserved for future use
reserved "UNKNOWN"; // Reserved name
INFORMATIONAL = 1;
SUCCESS = 2;
REDIRECTION = 3;
CLIENT_ERROR = 4;
SERVER_ERROR = 5;
}

Message Definition

Messages define structured data types with typed fields.

Basic Syntax

message Person {
string name = 1;
int32 age = 2;
}

With Explicit Type ID

message Person [id=101] {
string name = 1;
int32 age = 2;
}

Without Explicit Type ID

message Person {  // Auto-generated when enable_auto_type_id = true
string name = 1;
int32 age = 2;
}

Language Mapping

LanguageImplementation
JavaPOJO class with getters/setters
Python@dataclass class
GoStruct with exported fields
RustStruct with #[derive(ForyObject)]
C++Struct with FORY_STRUCT macro
JavaScriptexport interface declaration
Dart@ForyStruct final class

Type IDs control cross-language registration for messages, unions, and enums. See Type IDs for auto-generation, aliases, and collision handling.

Reserved Fields

Reserve field numbers or names to prevent reuse after removing fields:

message User {
reserved 2, 15, 9 to 11; // Reserved field numbers
reserved "old_field", "temp"; // Reserved field names
string id = 1;
string name = 3;
}

Message Type Options

Message-level options are declared inline in [] after the message name:

message User [deprecated=true] {
string id = 1;
string name = 2;
}

FDL does not support option ...; statements inside message or enum bodies.

Grammar:

message_def  := 'message' IDENTIFIER [type_options] '{' message_body '}'
type_options := '[' type_option (',' type_option)* ']'
type_option := IDENTIFIER '=' option_value
message_body := (reserved_stmt | nested_type | field_def)*
nested_type := enum_def | message_def | union_def

Rules:

  • Type IDs follow the rules in Type IDs.

Nested Types

Messages can contain nested message, enum, and union definitions. This is useful for defining types that are closely related to their parent message.

Nested Messages

message SearchResponse {
message Result {
string url = 1;
string title = 2;
list<string> snippets = 3;
}
list<Result> results = 1;
}

Nested Enums

message Container {
enum Status {
STATUS_UNKNOWN = 0;
STATUS_ACTIVE = 1;
STATUS_INACTIVE = 2;
}
Status status = 1;
}

Qualified Type Names

Nested types can be referenced from other messages using qualified names (Parent.Child):

message SearchResponse {
message Result {
string url = 1;
string title = 2;
}
}

message SearchResultCache {
// Reference nested type with qualified name
SearchResponse.Result cached_result = 1;
list<SearchResponse.Result> all_results = 2;
}

Deeply Nested Types

Nesting can be multiple levels deep:

message Outer {
message Middle {
message Inner {
string value = 1;
}
Inner inner = 1;
}
Middle middle = 1;
}

message OtherMessage {
// Reference deeply nested type
Outer.Middle.Inner deep_ref = 1;
}

Language-Specific Generation

LanguageNested Type Generation
JavaStatic inner classes (SearchResponse.Result)
PythonNested classes within dataclass
GoFlat structs with underscore (SearchResponse_Result, configurable to camelcase)
RustNested modules (search_response::Result)
C++Nested classes (SearchResponse::Result)
JavaScriptFlat names (Result)
DartFlat classes with underscore (SearchResponse_Result)

Note: Go defaults to underscore-separated nested names; set option go_nested_type_style = "camelcase"; to use concatenated names. Rust emits nested modules for nested types.

Nested Type Rules

  • Nested type names must be unique within their parent message
  • Nested types can have their own type IDs
  • Numeric type IDs must be globally unique (including nested types); see Type IDs for auto-generation and collision handling
  • Within a message, you can reference nested types by simple name
  • From outside, use the qualified name (Parent.Child)

Union Definition

Unions define a value that can hold exactly one of several case types.

Basic Syntax

union Animal [id=106] {
Dog dog = 1;
Cat cat = 2;
}

Using a Union in a Message

message Person [id=100] {
Animal pet = 1;
optional Animal favorite_pet = 2;
}

Rules

  • Case IDs must be unique within the union
  • Cases cannot be optional or ref
  • Union cases do not support field options
  • Case types can be primitives, enums, messages, or other named types
  • Union type IDs follow the rules in Type IDs.

Grammar:

union_def  := 'union' IDENTIFIER [type_options] '{' union_field* '}'
union_field := field_type IDENTIFIER '=' INTEGER ';'

Field Definition

Fields define the properties of a message.

Basic Syntax

field_type field_name = field_number;

With Modifiers

optional list<string> tags = 1;  // Nullable list
list<optional string> tags = 2; // Elements may be null
ref list<Node> nodes = 3; // Collection tracked as a reference
list<ref Node> nodes = 4; // Elements tracked as references

Grammar:

field_def    := [modifiers] field_type IDENTIFIER '=' INTEGER ';'
modifiers := { 'optional' | 'ref' }
field_type := primitive_type | named_type | list_type | array_type | map_type
list_type := 'list' '<' { 'optional' | 'ref' | scalar_encoding } field_type '>'
array_type := 'array' '<' array_element_type '>'

Modifiers apply to the field/collection. Use list<...> to describe element modifiers. repeated is accepted as an alias for list.

Field Modifiers

optional

Marks the field as nullable:

message User {
string name = 1; // Required, non-null
optional string email = 2; // Nullable
}

Generated Code:

LanguageNon-optionalOptional
JavaString nameString email with @ForyField(nullable=true)
Pythonname: strname: Optional[str]
GoName stringName *string
Rustname: Stringname: Option<String>
C++std::string namestd::optional<std::string> name
JavaScriptname: stringname?: string | null
DartString nameString? email

Default Values:

TypeDefault Value
Non-optional typesLanguage default
Optional typesnull/None/nil

ref

Enables reference tracking for shared/circular references:

message Node {
string value = 1;
ref Node parent = 2; // Can point to shared object
list<ref Node> children = 3;
}

Use Cases:

  • Shared objects (same object referenced multiple times)
  • Circular references (object graphs with cycles)
  • Tree structures with parent pointers

Generated Code:

LanguageWithout refWith ref
JavaNode parentNode parent with @ForyField(ref=true)
Pythonparent: Nodeparent: Node = pyfory.field(ref=True)
GoParent NodeParent *Node with fory:"ref"
Rustparent: Nodeparent: Arc<Node>
C++Node parentstd::shared_ptr<Node> parent
JavaScriptparent: Nodeparent: Node (no ref distinction)
DartNode parentNode parent with @ForyField(ref: true)

Rust uses Arc by default; use ref(thread_safe=false) or ref(weak=true) to customize pointer types. For protobuf option syntax, see Protocol Buffers IDL Support.

list

Marks the field as an ordered collection:

message Document {
list<string> tags = 1;
list<User> authors = 2;
}

Generated Code:

LanguageType
JavaList<String>
PythonList[str]
Go[]string
RustVec<String>
C++std::vector<std::string>
JavaScriptstring[]
DartList<String>

Combining Modifiers

Modifiers can be combined:

message Example {
optional list<string> tags = 1; // Nullable list
list<optional string> aliases = 2; // Elements may be null
ref list<Node> nodes = 3; // Collection tracked as a reference
list<ref Node> children = 4; // Elements tracked as references
optional ref User owner = 5; // Nullable tracked reference
}

Modifiers before list apply to the field/collection. Modifiers after list apply to elements. repeated is accepted as an alias for list.

List modifier mapping:

Fory IDLJavaPythonGoRustC++Dart
optional list<string>List<String> + @ForyField(nullable = true)Optional[List[str]][]string + nullableOption<Vec<String>>std::optional<std::vector<std::string>>List<String>?
list<optional string>List<String> (nullable elements)List[Optional[str]][]*stringVec<Option<String>>std::vector<std::optional<std::string>>List<String?>
ref list<User>List<User> + @ForyField(ref = true)List[User] + pyfory.field(ref=True)[]User + refArc<Vec<User>>std::shared_ptr<std::vector<User>>List<User> + @ForyField(ref: true)
list<ref User>List<User>List[User][]*User + ref=falseVec<Arc<User>>std::vector<std::shared_ptr<User>>List<User> + @ListField(element: DeclaredType(ref: true))

Use ref(thread_safe=false) in Fory IDL (or [(fory).thread_safe_pointer = false] in protobuf) to generate Rc instead of Arc in Rust.

Field Numbers

Each field must have a unique positive integer identifier:

message Example {
string first = 1;
string second = 2;
string third = 3;
}

Rules and best practices:

  • Numbers must be unique within a message.
  • Numbers must be positive integers.
  • Gaps are allowed and are useful when fields are removed.
  • Prefer sequential numbering from 1.
  • Never reuse a removed field number for a different field.

Type System

Fory IDL provides a cross-language type system for primitives, named types, and collections. Field modifiers (optional, ref) control nullability and reference tracking, while list<T> and array<T> choose collection schema kind (see Field Modifiers).

Primitive Types

TypeDescriptionSize
boolBoolean value1 byte
int8Signed 8-bit integer1 byte
int16Signed 16-bit integer2 bytes
int32Signed 32-bit integer, varint by default4 bytes
int64Signed 64-bit integer, PVL varint by default8 bytes
uint8Unsigned 8-bit integer1 byte
uint16Unsigned 16-bit integer2 bytes
uint32Unsigned 32-bit integer, varint by default4 bytes
uint64Unsigned 64-bit integer, PVL varint by default8 bytes
float16IEEE 754 binary16 floating point2 bytes
bfloat16Brain floating point2 bytes
float3232-bit floating point4 bytes
float6464-bit floating point8 bytes
stringUTF-8 stringVariable
bytesBinary dataVariable
dateCalendar dateVariable
timestampDate and time with timezoneVariable
durationDurationVariable
decimalDecimal valueVariable
anyDynamic value (runtime type)Variable

Boolean

LanguageTypeNotes
Javaboolean / BooleanPrimitive or boxed
Pythonbool
Gobool
Rustbool
C++bool
JavaScriptboolean
Dartbool

Integer Types

Fory IDL provides fixed-width signed integers (varint encoding for 32/64-bit by default):

Fory IDL TypeSizeRange
int88-bit-128 to 127
int1616-bit-32,768 to 32,767
int3232-bit-2^31 to 2^31 - 1
int6464-bit-2^63 to 2^63 - 1

Language Mapping (Signed):

Fory IDLJavaPythonGoRustC++JavaScriptDart
int8bytepyfory.Int8int8i8int8_tnumberint
int16shortpyfory.Int16int16i16int16_tnumberint
int32intpyfory.Int32int32i32int32_tnumberint
int64longpyfory.Int64int64i64int64_tbigint | numberInt64

Fory IDL provides fixed-width unsigned integers (varint encoding for 32/64-bit by default):

Fory IDLSizeRange
uint88-bit0 to 255
uint1616-bit0 to 65,535
uint3232-bit0 to 2^32 - 1
uint6464-bit0 to 2^64 - 1

Language Mapping (Unsigned):

Fory IDLJavaPythonGoRustC++JavaScriptDart
uint8shortpyfory.UInt8uint8u8uint8_tnumberint
uint16intpyfory.UInt16uint16u16uint16_tnumberint
uint32longpyfory.UInt32uint32u32uint32_tnumberint
uint64longpyfory.UInt64uint64u64uint64_tbigint | numberUint64

Integer Encoding Modifiers

For 32/64-bit integers, Fory IDL uses variable-length encoding by default. Add a scalar encoding modifier when you need a different wire encoding:

ModifierValid typesNotes
varintint32, int64, uint32, uint64Explicit spelling of default
fixedint32, int64, uint32, uint64Fixed-width little-endian
taggedint64, uint64Tagged 64-bit encoding

Modifiers are part of the scalar type expression, so they can be used in nested list and map positions:

fixed int32 id = 1;
list<fixed int32> offsets = 2;
map<string, tagged uint64> counters = 3;

Underscore spellings for integer encoding are not FDL type names.

Floating-Point Types

Fory IDL TypeSizePrecision
float3232-bit~7 digits
float6464-bit~15-16 digits

Language Mapping:

Fory IDLJavaPython annotation/valueGoRustC++JavaScriptDart
float16Float16pyfory.Float16 / floatfloat16.Float16Float16fory::float16_tnumberdouble
bfloat16BFloat16pyfory.BFloat16 / floatbfloat16.BFloat16BFloat16fory::bfloat16_tnumberdouble
float32floatpyfory.Float32float32f32floatnumberFloat32
float64doublepyfory.Float64float64f64doublenumberdouble

String Type

LanguageTypeNotes
JavaStringImmutable
Pythonstr
GostringImmutable
RustStringOwned, heap-allocated
C++std::string
JavaScriptstring
DartString

Bytes Type

LanguageTypeNotes
Javabyte[]
PythonbytesImmutable
Go[]byte
RustVec<u8>
C++std::vector<uint8_t>
JavaScriptUint8Array
DartUint8List

Temporal Types

Date
LanguageTypeNotes
Javajava.time.LocalDate
Pythondatetime.date
Gotime.TimeTime portion ignored
Rustchrono::NaiveDateRequires chrono crate
C++fory::serialization::Date
JavaScriptDate
DartLocalDateFory package type
Timestamp
LanguageTypeNotes
Javajava.time.InstantUTC-based
Pythondatetime.datetime
Gotime.Time
Rustchrono::NaiveDateTimeRequires chrono crate
C++fory::serialization::Timestamp
JavaScriptDate
DartTimestampFory package type

Any

LanguageTypeNotes
JavaObjectRuntime type written
PythonAnyRuntime type written
GoanyRuntime type written
RustBox<dyn Any>Runtime type written
C++std::anyRuntime type written
JavaScriptanyRuntime type written
DartObject?Runtime type written

Example:

enum EventType [id=120] {
CREATED = 0;
DELETED = 1;
}

message UserCreated [id=121] {
string user_id = 1;
}

message Envelope [id=122] {
EventType type = 1;
any payload = 2;
}

Generated Code (Envelope.payload):

LanguageGenerated Field Type
JavaObject payload
Pythonpayload: Any
GoPayload any
Rustpayload: Box<dyn Any>
C++std::any payload
JavaScriptpayload: any
DartObject? payload

Notes:

  • any always writes a null flag (same as nullable) because values may be empty.
  • Allowed runtime values are limited to bool, string, enum, message, and union. Other primitives (numeric, bytes, date/time) and list/map are not supported; wrap them in a message or use explicit fields instead.
  • ref is not allowed on any fields (including list/map values). Wrap any in a message if you need reference tracking.
  • The runtime type must be registered in the target language schema/IDL registration; unknown types fail to deserialize.

Named Types

Reference other messages, enums, or unions by name:

enum Status { ... }
message User { ... }

message Order {
User customer = 1; // Reference to User message
Status status = 2; // Reference to Status enum
}

Collection Types

List (list)

Use the list<...> type for list fields. repeated is accepted as an alias. See Field Modifiers for modifier combinations and language mapping.

Nested collection support is target-capability based. The C++ generator accepts nested collection specs such as list<list<...>>, list<map<...>>, and map<..., list<...>>; targets that have not implemented nested field specs continue to reject them. Use a message wrapper when you need portable schemas across all targets.

Array (array)

Use array<T> for dynamic-length dense numeric data. array<T> is a distinct schema kind from list<T> and uses the packed primitive-array wire payload.

message Embedding {
array<int32> indices = 1;
array<float32> values = 2;
array<uint8> pixels = 3;
}

array<T> accepts bool, integer, and floating-point element domains only. It does not accept optional, ref, named/object types, string, bytes, maps, or scalar integer encoding modifiers such as array<fixed int32>; array elements are always fixed-width by the array contract.

Generated carriers are language-specific, but the schema kind is not:

IDL schemaJava defaultPython defaultDart defaultJavaScript/TypeScript
list<bool>BoolList / List<Boolean>List[bool]List<bool>Type.list(Type.bool())
array<bool>boolean[]pyfory.BoolArrayBoolListType.boolArray()
array<int8>@Int8Type byte[]pyfory.Int8ArrayInt8ListType.int8Array()
array<int16>short[]pyfory.Int16ArrayInt16ListType.int16Array()
array<int32>int[]pyfory.Int32ArrayInt32ListType.int32Array()
array<int64>long[]pyfory.Int64ArrayInt64ListType.int64Array()
array<uint8>@UInt8Type byte[]pyfory.UInt8ArrayUint8ListType.uint8Array()
array<uint16>@UInt16Type short[]pyfory.UInt16ArrayUint16ListType.uint16Array()
array<uint32>@UInt32Type int[]pyfory.UInt32ArrayUint32ListType.uint32Array()
array<uint64>@UInt64Type long[]pyfory.UInt64ArrayUint64ListType.uint64Array()
array<float16>Float16Arraypyfory.Float16ArrayFloat16ListType.float16Array()
array<bfloat16>BFloat16Arraypyfory.BFloat16ArrayBfloat16ListType.bfloat16Array()
array<float32>float[]pyfory.Float32ArrayFloat32ListType.float32Array()
array<float64>double[]pyfory.Float64ArrayFloat64ListType.float64Array()

For handwritten Dart models, array<bool> requires BoolList plus @ArrayField(element: BoolType()) or @ForyField(type: ArrayType(element: BoolType())); List<bool> remains list<bool>. For handwritten Java models, unsigned primitive arrays use type-use annotations on the element type, for example private @UInt32Type int[] ids;.

Map

Maps with typed keys and values:

message Config {
map<string, string> properties = 1;
map<string, int32> counts = 2;
map<int32, User> users = 3;
}

Language Mapping:

Fory IDLJavaPythonGoRustC++JavaScriptDart
map<string, int32>Map<String, Integer>Dict[str, int]map[string]int32HashMap<String, i32>std::map<std::string, int32_t>Map<string, number>Map<String, int>
map<string, User>Map<String, User>Dict[str, User]map[string]UserHashMap<String, User>std::map<std::string, User>Map<string, User>Map<String, User>

Key Type Restrictions:

  • string (most common)
  • bool
  • Integer types (int8, int16, int32, int64, uint8, uint16, uint32, uint64)
  • Temporal scalar types (date, timestamp, duration)
  • Enums

Map keys do not support binary bytes, floating-point types, decimal, list<T>, array<T>, or nested map<K, V> types. Put those types in map values or wrap them in a message with a portable scalar or enum key.

Type Compatibility Matrix

This matrix shows which type conversions are safe across languages:

From -> Toboolint8int16int32int64float32float64string
boolYYYYY---
int8-YYYYYY-
int16--YYYYY-
int32---YY-Y-
int64----Y---
float32-----YY-
float64------Y-
string-------Y

Y = Safe conversion, - = Not recommended

Best Practices

  • Use int32 as the default for most integers; use int64 for large values.
  • Use string for text data (UTF-8) and bytes for binary data.
  • Use optional only when the field may legitimately be absent.
  • Use ref only when needed for shared or circular references.
  • Prefer list for ordered sequences and map for key-value lookups.

Type IDs

Type IDs enable efficient cross-language serialization and are used for messages, unions, and enums. When enable_auto_type_id = true (default) and id is omitted, the compiler auto-generates one using MurmurHash3(utf8(package.type_name)) (32-bit) and annotates it in generated code. When enable_auto_type_id = false, types without explicit IDs are registered by namespace and name instead. Collisions are detected at compile-time across the current file and all imports; when a collision occurs, the compiler raises an error and asks for an explicit id or an alias.

enum Color [id=100] { ... }
message User [id=101] { ... }
union Event [id=102] { ... }

Enum type IDs remain optional; if omitted they are auto-generated using the same hash when enable_auto_type_id = true.

With Explicit Type ID

message User [id=101] { ... }
message User [id=101, deprecated=true] { ... } // Multiple options

Without Explicit Type ID

message Config { ... }  // Auto-generated when enable_auto_type_id = true

You can set [alias="..."] to change the hash source without renaming the type.

Practical Notes

  • If a type omits id and enable_auto_type_id = true, Fory generates an ID with MurmurHash3(utf8(package.type_name)) (32-bit).
  • Package alias and type alias change the hash input and can be used to resolve hash collisions without renaming public types.
  • Manual IDs in the small varint range (0-127) are compact on the wire; auto IDs are typically larger and usually consume 4-5 bytes.

ID Assignment Strategy

// Enums: 100-199
enum Status [id=100] { ... }
enum Priority [id=101] { ... }

// User domain: 200-299
message User [id=200] { ... }
message UserProfile [id=201] { ... }

// Order domain: 300-399
message Order [id=300] { ... }
message OrderItem [id=301] { ... }

Complete Example

// E-commerce domain model
package com.shop.models;

// Enums with type IDs
enum OrderStatus [id=100] {
PENDING = 0;
CONFIRMED = 1;
SHIPPED = 2;
DELIVERED = 3;
CANCELLED = 4;
}

enum PaymentMethod [id=101] {
CREDIT_CARD = 0;
DEBIT_CARD = 1;
PAYPAL = 2;
BANK_TRANSFER = 3;
}

// Messages with type IDs
message Address [id=200] {
string street = 1;
string city = 2;
string state = 3;
string country = 4;
string postal_code = 5;
}

message Customer [id=201] {
string id = 1;
string name = 2;
optional string email = 3;
optional string phone = 4;
optional Address billing_address = 5;
optional Address shipping_address = 6;
}

message Product [id=202] {
string sku = 1;
string name = 2;
string description = 3;
float64 price = 4;
int32 stock = 5;
list<string> categories = 6;
map<string, string> attributes = 7;
}

message OrderItem [id=203] {
ref Product product = 1; // Track reference to avoid duplication
int32 quantity = 2;
float64 unit_price = 3;
}

message Order [id=204] {
string id = 1;
ref Customer customer = 2;
list<OrderItem> items = 3;
OrderStatus status = 4;
PaymentMethod payment_method = 5;
float64 total = 6;
optional string notes = 7;
timestamp created_at = 8;
optional timestamp shipped_at = 9;
}

// Config without explicit type ID (auto-generated when enable_auto_type_id = true)
message ShopConfig {
string store_name = 1;
string currency = 2;
float64 tax_rate = 3;
list<string> supported_countries = 4;
}

For protobuf-specific extension options and (fory). syntax, see Protocol Buffers IDL Support.

Grammar Summary

file         := [package_decl] file_option* import_decl* type_def*

package_decl := 'package' package_name ['alias' package_name] ';'
package_name := IDENTIFIER ('.' IDENTIFIER)*

file_option := 'option' option_name '=' option_value ';'
option_name := IDENTIFIER

import_decl := 'import' STRING ';'

type_def := enum_def | message_def | union_def

enum_def := 'enum' IDENTIFIER [type_options] '{' enum_body '}'
enum_body := (reserved_stmt | enum_value)*
enum_value := IDENTIFIER '=' INTEGER ';'

message_def := 'message' IDENTIFIER [type_options] '{' message_body '}'
message_body := (reserved_stmt | nested_type | field_def)*
nested_type := enum_def | message_def | union_def
field_def := [modifiers] field_type IDENTIFIER '=' INTEGER [field_options] ';'

union_def := 'union' IDENTIFIER [type_options] '{' union_field* '}'
union_field := ['repeated'] field_type IDENTIFIER '=' INTEGER [field_options] ';'
option_value := 'true' | 'false' | IDENTIFIER | INTEGER | STRING

reserved_stmt := 'reserved' reserved_items ';'
reserved_items := reserved_item (',' reserved_item)*
reserved_item := INTEGER | INTEGER 'to' INTEGER | INTEGER 'to' 'max' | STRING

modifiers := { 'optional' | 'ref' | 'repeated' }

field_type := [scalar_encoding] (primitive_type | named_type | list_type | array_type | map_type)
primitive_type := 'bool'
| 'int8' | 'int16' | 'int32' | 'int64'
| 'uint8' | 'uint16' | 'uint32' | 'uint64'
| 'float16' | 'bfloat16' | 'float32' | 'float64'
| 'string' | 'bytes'
| 'date' | 'timestamp' | 'duration' | 'decimal'
| 'any'
scalar_encoding := 'varint' | 'fixed' | 'tagged'
named_type := qualified_name
qualified_name := IDENTIFIER ('.' IDENTIFIER)* // e.g., Parent.Child
list_type := 'list' '<' { 'optional' | 'ref' | scalar_encoding } field_type '>'
array_type := 'array' '<' array_element_type '>'
map_type := 'map' '<' field_type ',' field_type '>'

type_options := '[' type_option (',' type_option)* ']'
type_option := IDENTIFIER '=' option_value // e.g., id=100, deprecated=true
field_options := '[' field_option (',' field_option)* ']'
field_option := IDENTIFIER '=' option_value // e.g., deprecated=true, ref=true

STRING := '"' [^"\n]* '"' | "'" [^'\n]* "'"
IDENTIFIER := [a-zA-Z_][a-zA-Z0-9_]*
INTEGER := '-'? [0-9]+