| --- |
| 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 |
| |
| 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 a complete reference for the Fory Definition Language (FDL) syntax. |
| |
| ## File Structure |
| |
| An FDL file consists of: |
| |
| 1. Optional package declaration |
| 2. Optional import statements |
| 3. Type definitions (enums and messages) |
| |
| ```protobuf |
| // 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] { ... } |
| ``` |
| |
| ## Comments |
| |
| FDL supports both single-line and block comments: |
| |
| ```protobuf |
| // 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. |
| |
| ```protobuf |
| package com.example.models; |
| ``` |
| |
| **Rules:** |
| |
| - Optional but recommended |
| - Must appear before any type definitions |
| - Only one package declaration per file |
| - Used for namespace-based type registration |
| |
| **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 `::`) | |
| |
| ## File-Level Options |
| |
| Options can be specified at file level to control language-specific code generation. |
| |
| ### Syntax |
| |
| ```protobuf |
| option option_name = value; |
| ``` |
| |
| ### Java Package Option |
| |
| Override the Java package for generated code: |
| |
| ```protobuf |
| 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 FDL package (`payment`) for cross-language compatibility |
| |
| ### Go Package Option |
| |
| Specify the Go import path and package name: |
| |
| ```protobuf |
| 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 FDL package (`payment`) for cross-language compatibility |
| |
| ### Java Outer Classname Option |
| |
| Generate all types as inner classes of a single outer wrapper class: |
| |
| ```protobuf |
| 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:** |
| |
| ```java |
| 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:** |
| |
| ```protobuf |
| 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: |
| |
| ```protobuf |
| 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 `.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):** |
| |
| ```protobuf |
| option java_outer_classname = "PaymentProtos"; |
| // Generates: PaymentProtos.java containing Payment and Receipt as inner classes |
| ``` |
| |
| **Example with java_multiple_files = true:** |
| |
| ```protobuf |
| option java_outer_classname = "PaymentProtos"; |
| option java_multiple_files = true; |
| // Generates: Payment.java, Receipt.java (separate files) |
| ``` |
| |
| ### Multiple Options |
| |
| Multiple options can be specified: |
| |
| ```protobuf |
| 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; |
| } |
| ``` |
| |
| ### Fory Extension Options |
| |
| FDL supports protobuf-style extension options for Fory-specific configuration: |
| |
| ```protobuf |
| 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](#fory-extension-options) section for complete documentation of message, enum, and field options. |
| |
| ### Option Priority |
| |
| For language-specific packages: |
| |
| 1. Command-line package override (highest priority) |
| 2. Language-specific option (`java_package`, `go_package`) |
| 3. FDL package declaration (fallback) |
| |
| **Example:** |
| |
| ```protobuf |
| 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: |
| |
| ```protobuf |
| 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 FDL files. |
| |
| ### Basic Syntax |
| |
| ```protobuf |
| import "path/to/file.fdl"; |
| ``` |
| |
| ### Multiple Imports |
| |
| ```protobuf |
| 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:** |
| |
| ```protobuf |
| 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:** |
| |
| ```protobuf |
| 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**: |
| |
| ```protobuf |
| // 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. |
| |
| ### 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 |
| |
| ```protobuf |
| enum Status { |
| PENDING = 0; |
| ACTIVE = 1; |
| COMPLETED = 2; |
| } |
| ``` |
| |
| ### With Type ID |
| |
| ```protobuf |
| enum Status [id=100] { |
| PENDING = 0; |
| ACTIVE = 1; |
| COMPLETED = 2; |
| } |
| ``` |
| |
| ### Reserved Values |
| |
| Reserve field numbers or names to prevent reuse: |
| |
| ```protobuf |
| enum Status { |
| reserved 2, 15, 9 to 11, 40 to max; // Reserved numbers |
| reserved "OLD_STATUS", "DEPRECATED"; // Reserved names |
| PENDING = 0; |
| ACTIVE = 1; |
| COMPLETED = 3; |
| } |
| ``` |
| |
| ### Enum Options |
| |
| Options can be specified within enums: |
| |
| ```protobuf |
| 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. |
| |
| ### 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: |
| |
| ```protobuf |
| // 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:** |
| |
| - 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 but recommended for cross-language use |
| |
| **Example with All Features:** |
| |
| ```protobuf |
| // 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 |
| |
| ```protobuf |
| message Person { |
| string name = 1; |
| int32 age = 2; |
| } |
| ``` |
| |
| ### With Type ID |
| |
| ```protobuf |
| message Person [id=101] { |
| string name = 1; |
| int32 age = 2; |
| } |
| ``` |
| |
| ### Reserved Fields |
| |
| Reserve field numbers or names to prevent reuse after removing fields: |
| |
| ```protobuf |
| message User { |
| reserved 2, 15, 9 to 11; // Reserved field numbers |
| reserved "old_field", "temp"; // Reserved field names |
| string id = 1; |
| string name = 3; |
| } |
| ``` |
| |
| ### Message Options |
| |
| Options can be specified within messages: |
| |
| ```protobuf |
| 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 |
| ``` |
| |
| ## Nested Types |
| |
| Messages can contain nested message and enum definitions. This is useful for defining types that are closely related to their parent message. |
| |
| ### Nested Messages |
| |
| ```protobuf |
| message SearchResponse { |
| message Result { |
| string url = 1; |
| string title = 2; |
| repeated string snippets = 3; |
| } |
| repeated Result results = 1; |
| } |
| ``` |
| |
| ### Nested Enums |
| |
| ```protobuf |
| 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): |
| |
| ```protobuf |
| 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; |
| } |
| ``` |
| |
| ### Deeply Nested Types |
| |
| Nesting can be multiple levels deep: |
| |
| ```protobuf |
| 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 (fory).go_nested_type_style = "camelcase";` to use concatenated names. Rust emits nested modules for nested types. |
| |
| ### Nested Type Rules |
| |
| - Nested type names must be unique within their parent message |
| - Nested types can have their own type IDs |
| - Type IDs must be globally unique (including nested types) |
| - 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 |
| |
| ```protobuf |
| union Animal [id=106] { |
| Dog dog = 1; |
| Cat cat = 2; |
| } |
| ``` |
| |
| ### Using a Union in a Message |
| |
| ```protobuf |
| 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`, `repeated`, or `ref` |
| - Union cases do not support field options |
| - Case types can be primitives, enums, messages, or other named types |
| - Union type IDs (`[id=...]`) are optional but recommended for cross-language use |
| |
| **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 |
| |
| ```protobuf |
| field_type field_name = field_number; |
| ``` |
| |
| ### With Modifiers |
| |
| ```protobuf |
| 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. |
| |
| ### Field Modifiers |
| |
| #### `optional` |
| |
| Marks the field as nullable: |
| |
| ```protobuf |
| 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` | |
| |
| #### `ref` |
| |
| Enables reference tracking for shared/circular references: |
| |
| ```protobuf |
| message Node { |
| string value = 1; |
| ref Node parent = 2; // Can point to shared object |
| repeated 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 `@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` | |
| |
| #### `repeated` |
| |
| Marks the field as a list/array: |
| |
| ```protobuf |
| 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>` | |
| |
| ### Combining Modifiers |
| |
| Modifiers can be combined: |
| |
| ```fdl |
| 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 System |
| |
| ### Primitive Types |
| |
| | 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](type-system.md) for complete type mappings. |
| |
| **Encoding notes:** |
| |
| - `int32`/`int64` and `uint32`/`uint64` use varint encoding by default. |
| - Use `fixed_*` for fixed-width integer encoding. |
| - Use `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. |
| |
| ### Named Types |
| |
| Reference other messages or enums by name: |
| |
| ```protobuf |
| enum Status { ... } |
| message User { ... } |
| |
| message Order { |
| User customer = 1; // Reference to User message |
| Status status = 2; // Reference to Status enum |
| } |
| ``` |
| |
| ### Map Types |
| |
| Maps with typed keys and values: |
| |
| ```protobuf |
| message Config { |
| map<string, string> properties = 1; |
| map<string, int32> counts = 2; |
| map<int32, User> users = 3; |
| } |
| ``` |
| |
| **Syntax:** `map<KeyType, ValueType>` |
| |
| **Restrictions:** |
| |
| - Key type should be a primitive type (typically `string` or integer types) |
| - Value type can be any type including messages |
| |
| ## Field Numbers |
| |
| Each field must have a unique positive integer identifier: |
| |
| ```protobuf |
| message Example { |
| string first = 1; |
| string second = 2; |
| string third = 3; |
| } |
| ``` |
| |
| **Rules:** |
| |
| - Must be unique within a message |
| - Must be positive integers |
| - Used for field ordering and identification |
| - Gaps in numbering are allowed (useful for deprecating fields) |
| |
| **Best Practices:** |
| |
| - Use sequential numbers starting from 1 |
| - Reserve number ranges for different categories |
| - Never reuse numbers for different fields (even after deletion) |
| |
| ## Type IDs |
| |
| Type IDs enable efficient cross-language serialization: |
| |
| ```protobuf |
| enum Color [id=100] { ... } |
| message User [id=101] { ... } |
| message Order [id=102] { ... } |
| ``` |
| |
| ### With Type ID (Recommended) |
| |
| ```protobuf |
| message User [id=101] { ... } |
| message User [id=101, deprecated=true] { ... } // Multiple options |
| ``` |
| |
| - Serialized as compact integer |
| - Fast lookup during deserialization |
| - Must be globally unique across all types |
| - Recommended for production use |
| |
| ### Without Type ID |
| |
| ```protobuf |
| message Config { ... } |
| ``` |
| |
| - Registered using namespace + name |
| - More flexible for development |
| - Slightly larger serialized size |
| - Uses package as namespace: `"package.Config"` |
| |
| ### ID Assignment Strategy |
| |
| ```protobuf |
| // 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 |
| |
| ```protobuf |
| // 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; |
| } |
| ``` |
| |
| ## Fory Extension Options |
| |
| FDL supports protobuf-style extension options for Fory-specific configuration. These use the `(fory)` prefix to indicate they are Fory extensions. |
| |
| ### File-Level Fory Options |
| |
| ```protobuf |
| 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 | |
| |
| ### Message-Level Fory Options |
| |
| Options can be specified inside the message body: |
| |
| ```protobuf |
| 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-Level Fory Options |
| |
| ```protobuf |
| 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-Level Fory Options |
| |
| Field options are specified in brackets after the field number (FDL uses `ref` modifiers instead |
| of bracket options for reference settings): |
| |
| ```protobuf |
| 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: |
| |
| ```fdl |
| message Graph { |
| ref(thread_safe = false) Node root = 1; |
| } |
| ``` |
| |
| ### Combining Standard and Fory Options |
| |
| You can combine standard options with Fory extension options: |
| |
| ```protobuf |
| 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]; |
| } |
| ``` |
| |
| ### Fory Options Proto File |
| |
| For reference, the Fory options are defined in `extension/fory_options.proto`: |
| |
| ```protobuf |
| // 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; |
| } |
| ``` |
| |
| ## Grammar Summary |
| |
| ``` |
| 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]+ |
| ``` |