title: Syntax Reference 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
This document provides a complete reference for the Fory Definition Language (FDL) syntax.
An FDL file consists of:
// Optional package declaration package com.example.models; // Import statements import "common/types.fdl"; // Type definitions enum Color [id=100] { ... } message User [id=101] { ... } message Order [id=102] { ... }
FDL 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;
Rules:
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 ::) |
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:
com/mycorp/payment/v1/ directorypackage com.mycorp.payment.v1;payment) for cross-language compatibilitySpecify 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:
package paymentv1payment) for cross-language compatibilityGenerate 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:
DescriptorProtos.java instead of separate filespublic static inner classespublic final with a private constructorGenerated 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:
.java filejava_outer_classname behaviorExample 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 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; }
FDL supports protobuf-style extension options for Fory-specific configuration:
option (fory).use_record_for_java_message = true; option (fory).polymorphism = true;
Available File Options:
| Option | Type | Description |
|---|---|---|
use_record_for_java_message | bool | Generate Java records instead of classes |
polymorphism | bool | Enable polymorphism for all types |
go_nested_type_style | string | Go nested type naming: underscore (default) or camelcase |
See the Fory Extension Options section for complete documentation of message, enum, and field options.
For language-specific packages:
java_package, go_package)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) |
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:
Import statements allow you to use types defined in other FDL 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:
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: FDL 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: FDL requires all imports to be present at compile time. Optional dependencies are not supported.
The compiler reports errors for:
import public or import weakEnums 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; }
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.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 := (option_stmt | reserved_stmt | enum_value)*
option_stmt := 'option' IDENTIFIER '=' option_value ';'
reserved_stmt := 'reserved' reserved_items ';'
enum_value := IDENTIFIER '=' INTEGER ';'
Rules:
[id=100]) is optional but recommended for cross-language useExample 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; }
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; }
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
Messages can contain nested message and enum 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; repeated string snippets = 3; } repeated 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; repeated 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) |
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.
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; }
optional, repeated, or ref[id=...]) are optional but recommended for cross-language useGrammar:
union_def := 'union' IDENTIFIER [type_options] '{' union_field* '}'
union_field := field_type IDENTIFIER '=' INTEGER ';'
Fields define the properties of a message.
field_type field_name = field_number;
optional repeated string tags = 1; // Nullable list repeated optional string tags = 2; // Elements may be null ref repeated Node nodes = 3; // Collection tracked as a reference repeated ref Node nodes = 4; // Elements tracked as references
Grammar:
field_def := [modifiers] field_type IDENTIFIER '=' INTEGER ';'
modifiers := { 'optional' | 'ref' } ['repeated' { 'optional' | 'ref' }]
field_type := primitive_type | named_type | map_type
Modifiers before repeated apply to the field/collection. Modifiers after repeated apply to list elements.
optionalMarks 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 | String email with @ForyField(nullable=true) |
| 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 |
refEnables reference tracking for shared/circular references:
message Node { string value = 1; ref Node parent = 2; // Can point to shared object repeated ref Node children = 3; }
Use Cases:
Generated Code:
| Language | Without ref | With ref |
|---|---|---|
| Java | Node parent | Node parent with @ForyField(ref=true) |
| Python | parent: Node | parent: Node = pyfory.field(ref=True) |
| Go | Parent Node | Parent *Node with fory:"ref" |
| Rust | parent: Node | parent: Arc<Node> |
| C++ | Node parent | std::shared_ptr<Node> parent |
repeatedMarks the field as a list/array:
message Document { repeated string tags = 1; repeated User authors = 2; }
Generated Code:
| Language | Type |
|---|---|
| Java | List<String> |
| Python | List[str] |
| Go | []string |
| Rust | Vec<String> |
| C++ | std::vector<std::string> |
Modifiers can be combined:
message Example { optional repeated string tags = 1; // Nullable list repeated optional string aliases = 2; // Elements may be null ref repeated Node nodes = 3; // Collection tracked as a reference repeated ref Node children = 4; // Elements tracked as references optional ref User owner = 5; // Nullable tracked reference }
Modifiers before repeated apply to the field/collection. Modifiers after repeated apply to elements.
| 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 encoding) | 4 bytes |
int64 | Signed 64-bit integer (varint encoding) | 8 bytes |
uint8 | Unsigned 8-bit integer | 1 byte |
uint16 | Unsigned 16-bit integer | 2 bytes |
uint32 | Unsigned 32-bit integer (varint encoding) | 4 bytes |
uint64 | Unsigned 64-bit integer (varint encoding) | 8 bytes |
fixed_int32 | Signed 32-bit integer (fixed encoding) | 4 bytes |
fixed_int64 | Signed 64-bit integer (fixed encoding) | 8 bytes |
fixed_uint32 | Unsigned 32-bit integer (fixed encoding) | 4 bytes |
fixed_uint64 | Unsigned 64-bit integer (fixed encoding) | 8 bytes |
tagged_int64 | Signed 64-bit integer (tagged encoding) | 8 bytes |
tagged_uint64 | Unsigned 64-bit integer (tagged encoding) | 8 bytes |
float16 | 16-bit 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 |
See Type System for complete type mappings.
Encoding notes:
int32/int64 and uint32/uint64 use varint encoding by default.fixed_* for fixed-width integer encoding.tagged_* for tagged/hybrid encoding (64-bit only).Any type notes:
any always writes a null flag (same as nullable) because the value may be empty.ref is not allowed on any fields. Wrap any in a message if you need reference tracking.Reference other messages or enums by name:
enum Status { ... } message User { ... } message Order { User customer = 1; // Reference to User message Status status = 2; // Reference to Status enum }
Maps with typed keys and values:
message Config { map<string, string> properties = 1; map<string, int32> counts = 2; map<int32, User> users = 3; }
Syntax: map<KeyType, ValueType>
Restrictions:
string or integer types)Each field must have a unique positive integer identifier:
message Example { string first = 1; string second = 2; string third = 3; }
Rules:
Best Practices:
Type IDs enable efficient cross-language serialization:
enum Color [id=100] { ... } message User [id=101] { ... } message Order [id=102] { ... }
message User [id=101] { ... } message User [id=101, deprecated=true] { ... } // Multiple options
message Config { ... }
"package.Config"// 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; repeated 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; repeated 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 type ID (uses namespace registration) message ShopConfig { string store_name = 1; string currency = 2; float64 tax_rate = 3; repeated string supported_countries = 4; }
FDL supports protobuf-style extension options for Fory-specific configuration. These use the (fory) prefix to indicate they are Fory extensions.
option (fory).use_record_for_java_message = true; option (fory).polymorphism = true;
| Option | Type | Description |
|---|---|---|
use_record_for_java_message | bool | Generate Java records instead of classes |
polymorphism | bool | Enable polymorphism for all types |
Options can be specified inside the message body:
message MyMessage { option (fory).id = 100; option (fory).evolving = false; option (fory).use_record_for_java = true; string name = 1; }
| Option | Type | Description |
|---|---|---|
id | int | Type ID for serialization (sets type_id) |
evolving | bool | Schema evolution support (default: true). When false, schema is fixed like a struct |
use_record_for_java | bool | Generate Java record for this message |
deprecated | bool | Mark this message as deprecated |
namespace | string | Custom namespace for type registration |
Note: option (fory).id = 100 is equivalent to the inline syntax message MyMessage [id=100].
enum Status { option (fory).id = 101; option (fory).deprecated = true; UNKNOWN = 0; ACTIVE = 1; }
| Option | Type | Description |
|---|---|---|
id | int | Type ID for serialization (sets type_id) |
deprecated | bool | Mark this enum as deprecated |
Field options are specified in brackets after the field number (FDL uses ref modifiers instead of bracket options for reference settings):
message Example { ref MyType friend = 1; string nickname = 2 [nullable = true]; ref MyType data = 3 [nullable = true]; ref(weak = true) MyType parent = 4; }
| Option | Type | Description |
|---|---|---|
ref | bool | Enable reference tracking (protobuf extension option) |
nullable | bool | Mark field as nullable (sets optional flag) |
deprecated | bool | Mark this field as deprecated |
thread_safe_pointer | bool | Rust only: use Arc (true) or Rc (false) for ref types |
weak_ref | bool | C++/Rust only: generate weak pointers for ref fields |
Note: For FDL, use ref (and optional ref(...)) modifiers: ref MyType friend = 1;, repeated ref(weak = true) Child children = 2;, map<string, ref(weak = true) Node> nodes = 3;. For protobuf, use [(fory).ref = true] and [(fory).weak_ref = true]. weak_ref is a codegen hint for C++/Rust and is ignored by Java/Python/Go. It must be used with ref (repeated ref for collections, or map<..., ref T> for map values).
To use Rc instead of Arc in Rust for a specific field:
message Graph { ref(thread_safe = false) Node root = 1; }
You can combine standard options with Fory extension options:
message User { option deprecated = true; // Standard option option (fory).evolving = false; // Fory extension option string name = 1; MyType data = 2 [deprecated = true, (fory).ref = true]; }
For reference, the Fory options are defined in extension/fory_options.proto:
// File-level options extend google.protobuf.FileOptions { optional ForyFileOptions fory = 50001; } message ForyFileOptions { optional bool use_record_for_java_message = 1; optional bool polymorphism = 2; } // Message-level options extend google.protobuf.MessageOptions { optional ForyMessageOptions fory = 50001; } message ForyMessageOptions { optional int32 id = 1; optional bool evolving = 2; optional bool use_record_for_java = 3; optional bool deprecated = 4; optional string namespace = 5; } // Field-level options extend google.protobuf.FieldOptions { optional ForyFieldOptions fory = 50001; } message ForyFieldOptions { optional bool ref = 1; optional bool nullable = 2; optional bool deprecated = 3; optional bool weak_ref = 4; }
file := [package_decl] file_option* import_decl* type_def*
package_decl := 'package' 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
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] ';'
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' } ['repeated' { 'optional' | 'ref' }]
field_type := primitive_type | named_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'
| 'float16' | 'float32' | 'float64'
| 'string' | 'bytes'
| 'date' | 'timestamp' | 'duration' | 'decimal'
| 'any'
named_type := qualified_name
qualified_name := IDENTIFIER ('.' IDENTIFIER)* // e.g., Parent.Child
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]+