跳到主要内容
版本: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 ::)

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

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 Compatibility Options

Fory IDL accepts protobuf-style extension syntax (for example, (fory).id) for compatibility, but native Fory IDL style uses plain option keys such as id, evolving, ref, and nullable without the (fory) prefix.

Equivalent forms:

// Native Fory IDL style (preferred in .fdl files)
message Node [id=100] {
ref Node parent = 1;
optional string nickname = 2;
}

// Protobuf-style compatibility syntax
message Node {
option (fory).id = 100;
Node parent = 1 [(fory).ref = true];
string nickname = 2 [(fory).nullable = true];
}

For the protobuf-specific extension option guide, see Protocol Buffers IDL Support.

Option Priority

For language-specific packages:

  1. Command-line package override (highest priority)
  2. Language-specific option (java_package, go_package)
  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 Options

Options can be specified within enums:

enum Status {
option deprecated = true; // Allowed
PENDING = 0;
ACTIVE = 1;
}

Forbidden Options:

  • option allow_alias = true 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 { ... }

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

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 := (option_stmt | reserved_stmt | enum_value)*
option_stmt := 'option' IDENTIFIER '=' option_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

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 Options

Options can be specified within messages:

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

Grammar:

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

Rules:

  • Type IDs follow the rules in Type IDs.

Nested Types

Messages can contain nested message and enum 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)

Note: Go defaults to underscore-separated nested names; set option (fory).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 | map_type
list_type := 'list' '<' { 'optional' | 'ref' } field_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

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

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 a list/array:

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>

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++
optional list<string>List<String> + @ForyField(nullable = true)Optional[List[str]][]string + nullableOption<Vec<String>>std::optional<std::vector<std::string>>
list<optional string>List<String> (nullable elements)List[Optional[str]][]*stringVec<Option<String>>std::vector<std::optional<std::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<ref User>List<User>List[User][]*User + ref=falseVec<Arc<User>>std::vector<std::shared_ptr<User>>

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, list, ref) control nullability, collection behavior, and reference tracking (see Field Modifiers).

Primitive Types

TypeDescriptionSize
boolBoolean value1 byte
int8Signed 8-bit integer1 byte
int16Signed 16-bit integer2 bytes
int32Signed 32-bit integer (varint encoding)4 bytes
int64Signed 64-bit integer (varint encoding)8 bytes
uint8Unsigned 8-bit integer1 byte
uint16Unsigned 16-bit integer2 bytes
uint32Unsigned 32-bit integer (varint encoding)4 bytes
uint64Unsigned 64-bit integer (varint encoding)8 bytes
fixed_int32Signed 32-bit integer (fixed encoding)4 bytes
fixed_int64Signed 64-bit integer (fixed encoding)8 bytes
fixed_uint32Unsigned 32-bit integer (fixed encoding)4 bytes
fixed_uint64Unsigned 64-bit integer (fixed encoding)8 bytes
tagged_int64Signed 64-bit integer (tagged encoding)8 bytes
tagged_uint64Unsigned 64-bit integer (tagged encoding)8 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

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++
int8bytepyfory.int8int8i8int8_t
int16shortpyfory.int16int16i16int16_t
int32intpyfory.int32int32i32int32_t
int64longpyfory.int64int64i64int64_t

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++
uint8shortpyfory.uint8uint8u8uint8_t
uint16intpyfory.uint16uint16u16uint16_t
uint32longpyfory.uint32uint32u32uint32_t
uint64longpyfory.uint64uint64u64uint64_t

Integer Encoding Variants

For 32/64-bit integers, Fory IDL uses varint encoding by default. Use explicit types when you need fixed-width or tagged encoding:

Fory IDL TypeEncodingNotes
fixed_int32fixedSigned 32-bit
fixed_int64fixedSigned 64-bit
fixed_uint32fixedUnsigned 32-bit
fixed_uint64fixedUnsigned 64-bit
tagged_int64taggedSigned 64-bit (hybrid)
tagged_uint64taggedUnsigned 64-bit (hybrid)

Floating-Point Types

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

Language Mapping:

Fory IDLJavaPythonGoRustC++
float32floatpyfory.float32float32f32float
float64doublepyfory.float64float64f64double

String Type

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

Bytes Type

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

Temporal Types

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

Any

LanguageTypeNotes
JavaObjectRuntime type written
PythonAnyRuntime type written
GoanyRuntime type written
RustBox<dyn Any>Runtime type written
C++std::anyRuntime 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

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 types are not supported. Use a message wrapper if you need list<list<...>>, list<map<...>>, or map<..., list<...>>.

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++
map<string, int32>Map<String, Integer>Dict[str, int]map[string]int32HashMap<String, i32>std::map<std::string, int32_t>
map<string, User>Map<String, User>Dict[str, User]map[string]UserHashMap<String, User>std::map<std::string, User>

Key Type Restrictions:

  • string (most common)
  • Integer types (int8, int16, int32, int64)
  • bool

Avoid using messages or complex types as keys.

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 | extension_name
extension_name := '(' IDENTIFIER ')' '.' IDENTIFIER // e.g., (fory).polymorphism

import_decl := 'import' STRING ';'

type_def := enum_def | message_def | union_def

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

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

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

option_stmt := 'option' option_name '=' option_value ';'
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' } ['list' { 'optional' | 'ref' }]

field_type := primitive_type | named_type | list_type | map_type
primitive_type := 'bool'
| 'int8' | 'int16' | 'int32' | 'int64'
| 'uint8' | 'uint16' | 'uint32' | 'uint64'
| 'fixed_int32' | 'fixed_int64' | 'fixed_uint32' | 'fixed_uint64'
| 'tagged_int64' | 'tagged_uint64'
| 'float32' | 'float64'
| 'string' | 'bytes'
| 'date' | 'timestamp' | 'duration' | 'decimal'
| 'any'
named_type := qualified_name
qualified_name := IDENTIFIER ('.' IDENTIFIER)* // e.g., Parent.Child
list_type := 'list' '<' { 'optional' | 'ref' } field_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 := option_name '=' option_value // e.g., deprecated=true, (fory).ref=true

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