| title | Schema IDL |
|---|---|
| sidebar_position | 2 |
| id | syntax |
| license | Licensed to the Apache Software Foundation (ASF) under one or more contributor license agreements. See the NOTICE file distributed with this work for additional information regarding copyright ownership. The ASF licenses this file to You under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. |
This 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.
An Fory IDL file typically consists of:
- Optional package declaration
- Optional file-level options
- Optional import statements
- Type definitions (enums, messages, and unions)
- Optional service definitions
// 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 OrderRequest [id=102] { ... }
message Order [id=103] { ... }
union Event [id=104] { ... }
// Service definitions
service OrderService {
rpc GetOrder (OrderRequest) returns (Order);
}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
}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 name-based type registration
- Package alias is used for auto-ID hashing
Language Mapping:
| Language | Package Usage |
|---|---|
| Java | Java package |
| Python | Module name (dots to underscores) |
| Go | Package name (last component) |
| Rust | Module name (dots to underscores) |
| C++ | Namespace (dots to ::) |
| C# | Namespace |
| JavaScript/TypeScript | TypeScript module name |
| Swift | Namespace wrapper or prefix |
| Dart | Library name (package segments) |
| Scala | Scala package |
| Kotlin | Kotlin package |
Options can be specified at file level to control language-specific code generation.
option option_name = value;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
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
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
Override the Kotlin package for generated source:
package payment;
option kotlin_package = "com.mycorp.payment.v1";
message Payment {
string id = 1;
}Effect:
- Generated Kotlin files are written under
com/mycorp/payment/v1/ - Kotlin source uses
package com.mycorp.payment.v1 - Type registration still uses the Fory IDL package (
payment) for cross-language compatibility
If kotlin_package is absent, Kotlin uses the FDL package. A Kotlin import
graph cannot mix default-package schemas with named Kotlin packages.
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_Payloadcamelcase:EnvelopePayload
The CLI flag --go_nested_type_style overrides this schema option when both are set.
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 examplePayment.V1.Payment)flatten: package prefix on top-level types (for examplePayment_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.
Rust generated code uses Fory's lightweight temporal carrier types by default:
fory::Date, fory::Timestamp, and fory::Duration. Set
rust_use_chrono_temporal_types when the generated Rust API should expose
chrono temporal types instead:
package payment;
option rust_use_chrono_temporal_types = true;
message Event {
date business_day = 1;
timestamp created_at = 2;
duration timeout = 3;
}With this option, Rust code maps date to chrono::NaiveDate, timestamp to
chrono::NaiveDateTime, and duration to chrono::Duration. The Rust crate
that compiles the generated code must depend on chrono and enable Fory's
chrono feature.
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.javainstead of separate files - All enums and messages become
public staticinner classes - The outer class is
public finalwith 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.
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_classname |
java_multiple_files |
Result |
|---|---|---|
| Not set | Any | Separate files (one per type) |
| Set | false (default) |
Single file with all types as inner classes |
| Set | true |
Separate files (overrides outer class) |
Effect of java_multiple_files = true:
- Each top-level enum and message gets its own
.javafile - Overrides
java_outer_classnamebehavior - 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 classesExample with java_multiple_files = true:
option java_outer_classname = "PaymentProtos";
option java_multiple_files = true;
// Generates: Payment.java, Receipt.java (separate files)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;
}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.
For language-specific packages/namespaces:
- Language-specific option (
java_package,go_package,csharp_namespace,kotlin_package) - Fory IDL package declaration (fallback)
Example:
package myapp.models;
option java_package = "com.example.generated";| Scenario | Java Package Used |
|---|---|
java_package present |
com.example.generated |
No java_package |
myapp.models (fallback) |
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 statements allow you to use types defined in other Fory IDL files.
import "path/to/file.fdl";import "common/types.fdl";
import "common/enums.fdl";
import "models/address.fdl";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)
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
}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.
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 publicorimport weak
Enums define a set of named integer constants.
enum Status {
PENDING = 0;
ACTIVE = 1;
COMPLETED = 2;
}enum Status [id=100] {
PENDING = 0;
ACTIVE = 1;
COMPLETED = 2;
}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-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_aliasis not supported. Each enum value must have a unique integer.
| Language | Implementation |
|---|---|
| Java | enum Status { UNKNOWN, ACTIVE, ... } |
| Python | class Status(IntEnum): UNKNOWN = 0 |
| Go | type Status int32 with constants |
| Rust | #[repr(i32)] enum Status { Unknown } |
| C++ | enum class Status : int32_t { ... } |
| C# | enum Status { Unknown, Active, ... } |
| JavaScript/TypeScript | export enum Status { UNKNOWN, ... } |
| Swift | enum Status with stable IDs |
| Dart | enum Status { unknown, active, ... } |
| Scala | Scala 3 enum Status |
| Kotlin | enum class Status |
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:
| Language | Output | Style |
|---|---|---|
| Java | UNKNOWN, TIER1, TIER2 |
Scoped enum |
| Rust | Unknown, Tier1, Tier2 |
Scoped enum |
| C++ | UNKNOWN, TIER1, TIER2 |
Scoped enum |
| Python | UNKNOWN, TIER1, TIER2 |
Scoped IntEnum |
| Go | DeviceTierUnknown, DeviceTierTier1, ... |
Unscoped const |
| JavaScript/TypeScript | UNKNOWN, TIER1, TIER2 |
Scoped enum |
| C# | Unknown, Tier1, Tier2 |
Scoped enum |
| Swift | unknown, tier1, tier2 |
Scoped enum |
| Dart | unknown, tier1, tier2 |
Scoped enum |
| Scala | Unknown, Tier1, Tier2 |
Scoped enum |
| Kotlin | UNKNOWN, TIER1, TIER2 |
Scoped 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;
}Messages define structured data types with typed fields.
message Person {
string name = 1;
int32 age = 2;
}message Person [id=101] {
string name = 1;
int32 age = 2;
}message Person { // Auto-generated when enable_auto_type_id = true
string name = 1;
int32 age = 2;
}| Language | Implementation |
|---|---|
| Java | POJO class with getters/setters |
| Python | @dataclass class |
| Go | Struct with exported fields |
| Rust | Struct with #[derive(ForyObject)] |
| C++ | Struct with FORY_STRUCT macro |
| C# | [ForyObject] class |
| JavaScript/TypeScript | export interface declaration |
| Swift | @ForyStruct struct or class |
| Dart | @ForyStruct final class |
| Scala | Scala 3 case class or class |
| Kotlin | data class or class |
Type IDs control cross-language registration for messages, unions, and enums. See Type IDs for auto-generation, aliases, and collision handling.
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-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.
Messages can contain nested message, enum, and union definitions. This is useful for defining types that are closely related to their parent message.
message SearchResponse {
message Result {
string url = 1;
string title = 2;
list<string> snippets = 3;
}
list<Result> results = 1;
}message Container {
enum Status {
STATUS_UNKNOWN = 0;
STATUS_ACTIVE = 1;
STATUS_INACTIVE = 2;
}
Status status = 1;
}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;
}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 | Nested Type Generation |
|---|---|
| Java | Static inner classes (SearchResponse.Result) |
| Python | Nested classes within dataclass |
| Go | Flat structs with underscore (SearchResponse_Result, configurable to camelcase) |
| Rust | Nested modules (search_response::Result) |
| C++ | Nested classes (SearchResponse::Result) |
| C# | Nested classes (SearchResponse.Result) |
| JavaScript/TypeScript | Flat names (Result) |
| Swift | Nested namespace wrappers or flattened names |
| Dart | Flat classes with underscore (SearchResponse_Result) |
| Scala | Nested companion/object scope |
| Kotlin | Flat generated names |
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 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)
Unions define a value that can hold exactly one of several case types.
union Animal [id=106] {
Dog dog = 1;
Cat cat = 2;
}message Person [id=100] {
Animal pet = 1;
optional Animal favorite_pet = 2;
}- Case IDs must be positive and unique within the union
- Case ID
0is reserved for language runtimes that expose an unknown-case carrier - Cases cannot be
optionalorref - 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 ';'
Services define RPC method contracts in Fory IDL. They are optional: schemas
with services still generate the normal data model types, and gRPC service code
is generated only when the compiler is run with --grpc for Java or Python.
message GetPetRequest [id=200] {
string name = 1;
}
message PetRecord [id=201] {
string name = 1;
Animal animal = 2;
}
service PetDirectory {
rpc GetPet (GetPetRequest) returns (PetRecord);
rpc Classify (Animal) returns (Animal);
}The first method uses message request and response types. The second method uses a direct union request and response type, which is supported in Fory IDL.
Use stream before the request type, the response type, or both:
service PetDirectory {
rpc GetPet (GetPetRequest) returns (PetRecord); // unary
rpc WatchPets (GetPetRequest) returns (stream PetRecord); // server streaming
rpc ImportPets (stream PetRecord) returns (PetRecord); // client streaming
rpc ChatPets (stream Animal) returns (stream Animal); // bidirectional streaming
}- Request and response types must reference named message or union types.
- Enum, primitive, collection, map, and array types are not valid direct RPC request or response types. Wrap those values in a message when they are part of a service contract.
- The generated Java and Python gRPC companions use Fory serialization for each
RPC payload. Applications that compile or run those companions provide their
own grpc-java or
grpciodependency.
Grammar:
service_def := 'service' IDENTIFIER '{' rpc_method* '}'
rpc_method := 'rpc' IDENTIFIER '(' ['stream'] named_type ')'
'returns' '(' ['stream'] named_type ')' ';'
Fields define the properties of a message.
field_type field_name = field_number;optional list<string> tags = 1; // Nullable list
list<optional string> tags = 2; // Elements may be null
list<ref Node> nodes = 3; // Elements tracked as referencesGrammar:
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 '>'
optional before list applies to the collection field. ref is only valid
for named message/union fields; for collection contents, use list<ref T> or
map<K, ref V>. repeated is accepted as an alias for list.
Marks the field as nullable:
message User {
string name = 1; // Required, non-null
optional string email = 2; // Nullable
}Generated Code:
| Language | Non-optional | Optional |
|---|---|---|
| Java | String name |
@Nullable String email |
| Python | name: str |
name: Optional[str] |
| Go | Name string |
Name *string |
| Rust | name: String |
name: Option<String> |
| C++ | std::string name |
std::optional<std::string> name |
| C# | string name |
string? email |
| JavaScript/TypeScript | name: string |
name?: string | null |
| Swift | String name |
String? email |
| Dart | String name |
String? email |
| Scala | name: String |
email: Option[String] |
| Kotlin | name: String |
email: String? |
Default Values:
| Type | Default Value |
|---|---|
| Non-optional types | Language default |
| Optional types | null/None/nil |
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:
| Language | Without ref |
With ref |
|---|---|---|
| Java | Node parent |
Node parent with @Ref |
| Python | parent: Node |
parent: Node = pyfory.field(ref=True) |
| Go | Parent Node |
Parent *Node with fory:"ref" |
| Rust | parent: Node |
parent: Rc<Node> |
| C++ | Node parent |
std::shared_ptr<Node> parent |
| C# | Node parent |
Node? parent with runtime ref tracking |
| JavaScript/TypeScript | parent: Node |
parent: Node (no ref distinction) |
| Swift | Node parent |
class reference with runtime ref tracking |
| Dart | Node parent |
Node parent with @ForyField(ref: true) |
| Scala | parent: Node |
@Ref parent: Node |
| Kotlin | parent: Node |
@Ref parent: Node? |
Rust uses Rc and RcWeak by default for ref-tracked fields. Use
ref(thread_safe=true) when the generated Rust type must use Arc or
ArcWeak for cross-thread shared ownership. This setting is a Rust codegen
carrier choice; it does not change the wire format or make the referenced value
itself thread-safe. For protobuf option syntax, see
Protocol Buffers IDL Support.
Rust pointer carrier mapping:
| Fory IDL | Rust type |
|---|---|
ref Node parent |
Rc<Node> |
ref(thread_safe=true) Node parent |
Arc<Node> |
ref(weak=true) Node parent |
RcWeak<Node> |
ref(weak=true, thread_safe=true) Node parent |
ArcWeak<Node> |
Marks the field as an ordered collection:
message Document {
list<string> tags = 1;
list<User> authors = 2;
}Generated Code:
| Language | Type |
|---|---|
| Java | List<String> |
| Python | List[str] |
| Go | []string |
| Rust | Vec<String> |
| C++ | std::vector<std::string> |
| C# | List<string> |
| JavaScript/TypeScript | string[] |
| Swift | [String] |
| Dart | List<String> |
| Scala | List[String] |
| Kotlin | List<String> |
Modifiers can be combined:
message Example {
optional list<string> tags = 1; // Nullable list
list<optional string> aliases = 2; // Elements may be null
list<ref Node> children = 3; // Elements tracked as references
optional ref User owner = 4; // Nullable tracked reference
}
optional before list applies to the field/collection. ref before list or
map is invalid; put ref inside the element/value type instead. repeated is
accepted as an alias for list.
List modifier mapping:
| Fory IDL | Java | Python | Go | Rust | C++ | Dart | Scala |
|---|---|---|---|---|---|---|---|
optional list<string> |
@Nullable List<String> |
Optional[List[str]] |
[]string + nullable |
Option<Vec<String>> |
std::optional<std::vector<std::string>> |
List<String>? |
Option[List[String]] |
list<optional string> |
List<String> (nullable elements) |
List[Optional[str]] |
[]*string |
Vec<Option<String>> |
std::vector<std::optional<std::string>> |
List<String?> |
List[Option[String]] |
list<ref User> |
List<@Ref User> |
List[User] |
[]*User + ref=false |
Vec<Rc<User>> |
std::vector<std::shared_ptr<User>> |
List<User> + @ListField(element: DeclaredType(ref: true)) |
List[User @Ref] |
Use ref(thread_safe=true) in Fory IDL (or [(fory).thread_safe_pointer = true] in protobuf)
to generate Arc instead of Rc in Rust.
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.
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).
The compact tables in this section show common generated carriers. For the complete 1.0 runtime surface, including C#, Swift, Dart, Scala, and Kotlin, see the xlang type-mapping specification.
| Type | Description | Size |
|---|---|---|
bool |
Boolean value | 1 byte |
int8 |
Signed 8-bit integer | 1 byte |
int16 |
Signed 16-bit integer | 2 bytes |
int32 |
Signed 32-bit integer, varint by default | 4 bytes |
int64 |
Signed 64-bit integer, PVL varint by default | 8 bytes |
uint8 |
Unsigned 8-bit integer | 1 byte |
uint16 |
Unsigned 16-bit integer | 2 bytes |
uint32 |
Unsigned 32-bit integer, varint by default | 4 bytes |
uint64 |
Unsigned 64-bit integer, PVL varint by default | 8 bytes |
float16 |
IEEE 754 binary16 floating point | 2 bytes |
bfloat16 |
Brain floating point | 2 bytes |
float32 |
32-bit floating point | 4 bytes |
float64 |
64-bit floating point | 8 bytes |
string |
UTF-8 string | Variable |
bytes |
Binary data | Variable |
date |
Calendar date | Variable |
timestamp |
Date and time with timezone | Variable |
duration |
Duration | Variable |
decimal |
Decimal value | Variable |
any |
Dynamic value (runtime type) | Variable |
| Language | Type | Notes |
|---|---|---|
| Java | boolean / Boolean |
Primitive or boxed |
| Python | bool |
|
| Go | bool |
|
| Rust | bool |
|
| C++ | bool |
|
| JavaScript/TypeScript | boolean |
|
| Dart | bool |
Fory IDL provides fixed-width signed integers (varint encoding for 32/64-bit by default):
| Fory IDL Type | Size | Range |
|---|---|---|
int8 |
8-bit | -128 to 127 |
int16 |
16-bit | -32,768 to 32,767 |
int32 |
32-bit | -2^31 to 2^31 - 1 |
int64 |
64-bit | -2^63 to 2^63 - 1 |
Language Mapping (Signed):
| Fory IDL | Java | Python | Go | Rust | C++ | JavaScript/TypeScript | Dart |
|---|---|---|---|---|---|---|---|
int8 |
byte |
pyfory.Int8 |
int8 |
i8 |
int8_t |
number |
int |
int16 |
short |
pyfory.Int16 |
int16 |
i16 |
int16_t |
number |
int |
int32 |
int |
pyfory.Int32 |
int32 |
i32 |
int32_t |
number |
int |
int64 |
long |
pyfory.Int64 |
int64 |
i64 |
int64_t |
bigint | number |
Int64 |
Fory IDL provides fixed-width unsigned integers (varint encoding for 32/64-bit by default):
| Fory IDL | Size | Range |
|---|---|---|
uint8 |
8-bit | 0 to 255 |
uint16 |
16-bit | 0 to 65,535 |
uint32 |
32-bit | 0 to 2^32 - 1 |
uint64 |
64-bit | 0 to 2^64 - 1 |
Language Mapping (Unsigned):
| Fory IDL | Java | Python | Go | Rust | C++ | JavaScript/TypeScript | Dart |
|---|---|---|---|---|---|---|---|
uint8 |
short |
pyfory.UInt8 |
uint8 |
u8 |
uint8_t |
number |
int |
uint16 |
int |
pyfory.UInt16 |
uint16 |
u16 |
uint16_t |
number |
int |
uint32 |
long |
pyfory.UInt32 |
uint32 |
u32 |
uint32_t |
number |
int |
uint64 |
long |
pyfory.UInt64 |
uint64 |
u64 |
uint64_t |
bigint | number |
Uint64 |
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:
| Modifier | Valid types | Notes |
|---|---|---|
varint |
int32, int64, uint32, uint64 |
Explicit spelling of default |
fixed |
int32, int64, uint32, uint64 |
Fixed-width little-endian |
tagged |
int64, uint64 |
Tagged 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.
| Fory IDL Type | Size | Precision |
|---|---|---|
float32 |
32-bit | ~7 digits |
float64 |
64-bit | ~15-16 digits |
Language Mapping:
| Fory IDL | Java | Python annotation/value | Go | Rust | C++ | JavaScript/TypeScript | Dart |
|---|---|---|---|---|---|---|---|
float16 |
Float16 |
pyfory.Float16 / float |
float16.Float16 |
Float16 |
fory::float16_t |
number |
double |
bfloat16 |
BFloat16 |
pyfory.BFloat16 / float |
bfloat16.BFloat16 |
BFloat16 |
fory::bfloat16_t |
number |
double |
float32 |
float |
pyfory.Float32 |
float32 |
f32 |
float |
number |
Float32 |
float64 |
double |
pyfory.Float64 |
float64 |
f64 |
double |
number |
double |
| Language | Type | Notes |
|---|---|---|
| Java | String |
Immutable |
| Python | str |
|
| Go | string |
Immutable |
| Rust | String |
Owned, heap-allocated |
| C++ | std::string |
|
| JavaScript/TypeScript | string |
|
| Dart | String |
| Language | Type | Notes |
|---|---|---|
| Java | byte[] |
|
| Python | bytes |
Immutable |
| Go | []byte |
|
| Rust | Vec<u8> |
|
| C++ | std::vector<uint8_t> |
|
| JavaScript/TypeScript | Uint8Array |
|
| Dart | Uint8List |
| Language | Type | Notes |
|---|---|---|
| Java | java.time.LocalDate |
|
| Python | datetime.date |
|
| Go | time.Time |
Time portion ignored |
| Rust | fory::Date |
Set rust_use_chrono_temporal_types = true to generate chrono::NaiveDate |
| C++ | fory::serialization::Date |
|
| JavaScript/TypeScript | Date |
|
| Dart | LocalDate |
Fory package type |
| Language | Type | Notes |
|---|---|---|
| Java | java.time.Instant |
UTC-based |
| Python | datetime.datetime |
|
| Go | time.Time |
|
| Rust | fory::Timestamp |
Set rust_use_chrono_temporal_types = true to generate chrono::NaiveDateTime |
| C++ | fory::serialization::Timestamp |
|
| JavaScript/TypeScript | Date |
|
| Dart | Timestamp |
Fory package type |
| Language | Type | Notes |
|---|---|---|
| Java | java.time.Duration |
|
| Python | datetime.timedelta |
|
| Go | time.Duration |
|
| Rust | fory::Duration |
Set rust_use_chrono_temporal_types = true to generate chrono::Duration |
| C++ | fory::serialization::Duration |
|
| Dart | Duration |
| Language | Type | Notes |
|---|---|---|
| Java | Object |
Runtime type written |
| Python | Any |
Runtime type written |
| Go | any |
Runtime type written |
| Rust | Box<dyn Any> |
Runtime type written |
| C++ | std::any |
Runtime type written |
| JavaScript/TypeScript | any |
Runtime type written |
| Dart | Object? |
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):
| Language | Generated Field Type |
|---|---|
| Java | Object payload |
| Python | payload: Any |
| Go | Payload any |
| Rust | payload: Box<dyn Any> |
| C++ | std::any payload |
| JavaScript/TypeScript | payload: any |
| Dart | Object? payload |
Notes:
anyalways writes a null flag (same asnullable) because values may be empty.- Allowed runtime values are limited to
bool,string,enum,message, andunion. Other primitives (numeric, bytes, date/time) and list/map are not supported; wrap them in a message or use explicit fields instead. refis not allowed onanyfields (including list/map values). Wrapanyin 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.
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
}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.
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 schema | Java default | Python default | Dart default | JavaScript/TypeScript |
|---|---|---|---|---|
list<bool> |
BoolList / List<Boolean> |
List[bool] |
List<bool> |
Type.list(Type.bool()) |
array<bool> |
boolean[] |
pyfory.BoolArray |
BoolList |
Type.boolArray() |
array<int8> |
@Int8Type byte[] |
pyfory.Int8Array |
Int8List |
Type.int8Array() |
array<int16> |
short[] |
pyfory.Int16Array |
Int16List |
Type.int16Array() |
array<int32> |
int[] |
pyfory.Int32Array |
Int32List |
Type.int32Array() |
array<int64> |
long[] |
pyfory.Int64Array |
Int64List |
Type.int64Array() |
array<uint8> |
@UInt8Type byte[] |
pyfory.UInt8Array |
Uint8List |
Type.uint8Array() |
array<uint16> |
@UInt16Type short[] |
pyfory.UInt16Array |
Uint16List |
Type.uint16Array() |
array<uint32> |
@UInt32Type int[] |
pyfory.UInt32Array |
Uint32List |
Type.uint32Array() |
array<uint64> |
@UInt64Type long[] |
pyfory.UInt64Array |
Uint64List |
Type.uint64Array() |
array<float16> |
Float16Array |
pyfory.Float16Array |
Float16List |
Type.float16Array() |
array<bfloat16> |
BFloat16Array |
pyfory.BFloat16Array |
Bfloat16List |
Type.bfloat16Array() |
array<float32> |
float[] |
pyfory.Float32Array |
Float32List |
Type.float32Array() |
array<float64> |
double[] |
pyfory.Float64Array |
Float64List |
Type.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;.
For generated Kotlin models, array<int8> uses @ArrayType ByteArray,
including nested collection and map positions.
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 IDL | Java | Python | Go | Rust | C++ | JavaScript/TypeScript | Dart |
|---|---|---|---|---|---|---|---|
map<string, int32> |
Map<String, Integer> |
Dict[str, int] |
map[string]int32 |
HashMap<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]User |
HashMap<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.
This matrix shows which type conversions are safe across languages:
| From -> To | bool | int8 | int16 | int32 | int64 | float32 | float64 | string |
|---|---|---|---|---|---|---|---|---|
| bool | Y | Y | Y | Y | Y | - | - | - |
| int8 | - | Y | Y | Y | Y | Y | Y | - |
| int16 | - | - | Y | Y | Y | Y | Y | - |
| int32 | - | - | - | Y | Y | - | Y | - |
| int64 | - | - | - | - | Y | - | - | - |
| float32 | - | - | - | - | - | Y | Y | - |
| float64 | - | - | - | - | - | - | Y | - |
| string | - | - | - | - | - | - | - | Y |
Y = Safe conversion, - = Not recommended
- Use
int32as the default for most integers; useint64for large values. - Use
stringfor text data (UTF-8) andbytesfor binary data. - Use
optionalonly when the field may legitimately be absent. - Use
refonly when needed for shared or circular references. - Prefer
listfor ordered sequences andmapfor key-value lookups.
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.
For Java and Scala generated code, nested name registration appends the parent
path to the namespace and keeps the nested type's simple name. For example,
package demo; message Envelope { message Payload { ... } } registers
Payload as namespace demo.Envelope and type name Payload in those JVM
targets.
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.
message User [id=101] { ... }
message User [id=101, deprecated=true] { ... } // Multiple optionsmessage Config { ... } // Auto-generated when enable_auto_type_id = trueYou can set [alias="..."] to change the hash source without renaming the type.
- If a type omits
idandenable_auto_type_id = true, Fory generates an ID withMurmurHash3(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.
// 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] { ... }// 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.
file := [package_decl] file_option* import_decl* definition*
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 ';'
definition := type_def | service_def
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] ';'
service_def := 'service' IDENTIFIER '{' rpc_method* '}'
rpc_method := 'rpc' IDENTIFIER '(' ['stream'] named_type ')'
'returns' '(' ['stream'] named_type ')' ';'
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]+