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:
- Optional package declaration
- Optional file-level options
- Optional import statements
- 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:
| 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 |
File-Level Options
Options can be specified at file level to control language-specific code generation.
Syntax
option option_name = value;
Java Package Option
Override the Java package for generated code:
package payment;
option java_package = "com.mycorp.payment.v1";
message Payment {
string id = 1;
}
Effect:
- Generated Java files will be in
com/mycorp/payment/v1/directory - Java package declaration will be
package com.mycorp.payment.v1; - Type registration still uses the Fory IDL package (
payment) for cross-language compatibility
Go Package Option
Specify the Go import path and package name:
package payment;
option go_package = "github.com/mycorp/apis/gen/payment/v1;paymentv1";
message Payment {
string id = 1;
}
Format: "import/path;package_name" or just "import/path" (last segment used as package name)
Effect:
- Generated Go files will have
package paymentv1 - The import path can be used in other Go code
- Type registration still uses the Fory IDL package (
payment) for cross-language compatibility
C# Namespace Option
Override the C# namespace for generated code:
package payment;
option csharp_namespace = "MyCorp.Payment.V1";
message Payment {
string id = 1;
}
Effect:
- Generated C# files use
namespace MyCorp.Payment.V1; - Output path follows namespace segments (
MyCorp/Payment/V1/under--csharp_out) - Type registration still uses the Fory IDL package (
payment) for cross-language compatibility
Go Nested Type Style Option
Control Go naming for nested message/enum/union types:
package payment;
option go_nested_type_style = "camelcase";
message Envelope {
message Payload {
string id = 1;
}
}
Values:
underscore(default):Envelope_Payloadcamelcase:EnvelopePayload
The CLI flag --go_nested_type_style overrides this schema option when both are set.
Swift Namespace Style Option
Control how package namespace is reflected in Swift generated type names:
package payment.v1;
option swift_namespace_style = "flatten";
message Payment {
string id = 1;
}
Values:
enum(default): namespace wrappers (for 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.
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.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.
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_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 classes
Example with java_multiple_files = true:
option java_outer_classname = "PaymentProtos";
option java_multiple_files = true;
// Generates: Payment.java, Receipt.java (separate files)
Multiple Options
Multiple options can be specified:
package payment;
option java_package = "com.mycorp.payment.v1";
option go_package = "github.com/mycorp/apis/gen/payment/v1;paymentv1";
option deprecated = true;
message Payment {
string id = 1;
}
Protobuf Extension Syntax
In .fdl files, use native Fory IDL syntax only (for example, [id=100], ref,
optional, nullable=true).
Protobuf extension syntax with (fory). is for .proto files and the protobuf
frontend only.
For protobuf extension options, see Protocol Buffers IDL Support.
Option Priority
For language-specific packages/namespaces:
- Command-line package override (highest priority)
- Language-specific option (
java_package,go_package,csharp_namespace) - Fory IDL package declaration (fallback)
Example:
package myapp.models;
option java_package = "com.example.generated";
| Scenario | Java Package Used |
|---|---|
| No override | com.example.generated |
CLI: --package=override | override |
| No java_package option | myapp.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 publicorimport 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_aliasis not supported. Each enum value must have a unique integer.
Language Mapping
| 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 { ... } |
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:
| 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 |
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
| 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 |
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
| 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) |
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.