Skip to main content
Version: 0.15

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 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:

  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 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 { ... }

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 := (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

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)

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

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 := 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 := IDENTIFIER '=' option_value // e.g., deprecated=true, ref=true

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