Field Configuration
This page explains how to configure field-level metadata for serialization in Rust.
Overview
Apache Fory™ provides the #[fory(...)] attribute macro to specify optional field-level metadata at compile time. This enables:
- Tag IDs: Assign compact numeric IDs to minimize struct field meta size overhead
- Nullability: Control whether fields can be null
- Reference Tracking: Enable reference tracking for shared ownership types
- Field Skipping: Exclude fields from serialization
- Encoding Control: Specify how integers are encoded (varint, fixed, tagged)
Basic Syntax
The #[fory(...)] attribute is placed on individual struct fields:
use fory::Fory;
#[derive(Fory)]
struct Person {
#[fory(id = 0)]
name: String,
#[fory(id = 1)]
age: i32,
#[fory(id = 2, nullable)]
nickname: Option<String>,
}
Multiple options are separated by commas.
Available Options
Field ID (id = N)
Assigns a numeric ID to a field to minimize struct field meta size overhead:
#[derive(Fory)]
struct User {
#[fory(id = 0)]
id: i64,
#[fory(id = 1)]
name: String,
#[fory(id = 2)]
age: i32,
}
Benefits:
- Smaller serialized size (numeric IDs vs field names in metadata)
- Allows renaming fields without breaking binary compatibility
Recommendation: It is recommended to configure field IDs for compatible mode since it reduces serialization cost.
Notes:
- IDs must be unique within a struct
- IDs must be >= 0 (use -1 to explicitly opt-out of tag ID encoding)
- If not specified, field name is used in metadata (larger overhead)
Skipping Fields (skip)
Excludes a field from serialization:
#[derive(Fory)]
struct User {
#[fory(id = 0)]
id: i64,
#[fory(id = 1)]
name: String,
#[fory(skip)]
password: String, // Not serialized
}
The password field will not be included in serialized output and will remain at its default value after deserialization.
Nullable (nullable)
Controls whether null flags are written for fields:
use fory::{Fory, RcWeak};
#[derive(Fory)]
struct Record {
// RcWeak is nullable by default, override to non-nullable
#[fory(id = 0, nullable = false)]
required_ref: RcWeak<Data>,
}
Default Behavior:
| Type | Default Nullable |
|---|---|
Option<T> | true |
RcWeak<T>, ArcWeak<T> | true |
| All other types | false |
Notes:
- For
Option<T>,RcWeak<T>,ArcWeak<T>, nullable defaults to true - For all other types, nullable defaults to false
- Use
nullable = falseto override defaults for types that are nullable by default
Reference Tracking (ref)
Controls per-field reference tracking for shared ownership types:
use std::rc::Rc;
use std::sync::Arc;
#[derive(Fory)]
struct Container {
// Enable reference tracking (default for Rc/Arc)
#[fory(id = 0, ref = true)]
shared_data: Rc<Data>,
// Disable reference tracking
#[fory(id = 1, ref = false)]
unique_data: Rc<Data>,
}
Default Behavior:
| Type | Default Ref Tracking |
|---|---|
Rc<T>, Arc<T> | true |
RcWeak<T>, ArcWeak<T> | true |
Option<Rc<T>>, Option<Arc<T>> | true (inherited) |
| All other types | false |
Use Cases:
- Enable for fields that may be circular or shared
- Disable for fields that are always unique (optimization)
Encoding (encoding)
Controls how integer fields are encoded:
#[derive(Fory)]
struct Metrics {
// Variable-length encoding (smaller for small values)
#[fory(id = 0, encoding = "varint")]
count: i64,
// Fixed-length encoding (consistent size)
#[fory(id = 1, encoding = "fixed")]
timestamp: i64,
// Tagged encoding (includes type tag, u64 only)
#[fory(id = 2, encoding = "tagged")]
value: u64,
}
Supported Encodings:
| Type | Options | Default |
|---|---|---|
i32, u32 | varint, fixed | varint |
i64, u64 | varint, fixed, tagged | varint |
When to Use:
varint: Best for values that are often small (default)fixed: Best for values that use full range (e.g., timestamps, hashes)tagged: When type information needs to be preserved (u64 only)
Compress (compress)
A convenience shorthand for controlling integer encoding:
#[derive(Fory)]
struct Data {
// compress = true -> varint encoding (default)
#[fory(id = 0, compress)]
small_value: i32,
// compress = false -> fixed encoding
#[fory(id = 1, compress = false)]
fixed_value: u32,
}
Notes:
compressorcompress = trueis equivalent toencoding = "varint"compress = falseis equivalent toencoding = "fixed"- If both
compressandencodingare specified, they must not conflict
Type Classification
Fory classifies field types to determine default behavior:
| Type Class | Examples | Default Nullable | Default Ref |
|---|---|---|---|
| Primitive | i8, i32, f64, bool | false | false |
| Option | Option<T> | true | false |
| Rc | Rc<T> | false | true |
| Arc | Arc<T> | false | true |
| RcWeak | RcWeak<T> (fory type) | true | true |
| ArcWeak | ArcWeak<T> (fory type) | true | true |
| Other | String, Vec<T>, user types | false | false |
Special Case: Option<Rc<T>> and Option<Arc<T>> inherit the inner type's ref tracking behavior.
Complete Example
use fory::Fory;
use std::rc::Rc;
#[derive(Fory, Default)]
struct Document {
// Required fields with tag IDs
#[fory(id = 0)]
title: String,
#[fory(id = 1)]
version: i32,
// Optional field (nullable by default for Option)
#[fory(id = 2)]
description: Option<String>,
// Reference-tracked shared pointer
#[fory(id = 3)]
parent: Rc<Document>,
// Nullable + reference-tracked
#[fory(id = 4, nullable)]
related: Option<Rc<Document>>,
// Counter with varint encoding (small values)
#[fory(id = 5, encoding = "varint")]
view_count: u64,
// Timestamp with fixed encoding (full range values)
#[fory(id = 6, encoding = "fixed")]
created_at: i64,
// Skip sensitive field
#[fory(skip)]
internal_state: String,
}
fn main() {
let fory = fory::Fory::default();
let doc = Document {
title: "My Document".to_string(),
version: 1,
description: Some("A sample document".to_string()),
parent: Rc::new(Document::default()),
related: None, // Allowed because nullable
view_count: 42,
created_at: 1704067200,
internal_state: "secret".to_string(), // Will be skipped
};
let bytes = fory.serialize(&doc);
let decoded: Document = fory.deserialize(&bytes).unwrap();
}
Compile-Time Validation
Invalid configurations are caught at compile time:
// Error: duplicate field IDs
#[derive(Fory)]
struct Bad {
#[fory(id = 0)]
field1: String,
#[fory(id = 0)] // Compile error: duplicate id
field2: String,
}
// Error: invalid id value
#[derive(Fory)]
struct Bad2 {
#[fory(id = -2)] // Compile error: id must be >= -1
field: String,
}
// Error: conflicting encoding attributes
#[derive(Fory)]
struct Bad3 {
#[fory(compress = true, encoding = "fixed")] // Compile error: conflict
field: i32,
}
Cross-Language Compatibility
When serializing data to be read by other languages (Java, C++, Go, Python), use field configuration to match encoding expectations:
#[derive(Fory)]
struct CrossLangData {
// Matches Java Integer with varint
#[fory(id = 0, encoding = "varint")]
int_var: i32,
// Matches Java Integer with fixed
#[fory(id = 1, encoding = "fixed")]
int_fixed: i32,
// Matches Java Long with tagged encoding
#[fory(id = 2, encoding = "tagged")]
long_tagged: u64,
// Nullable pointer matches Java nullable reference
#[fory(id = 3, nullable)]
optional: Option<String>,
}
Schema Evolution
Compatible mode supports schema evolution. It is recommended to configure field IDs to reduce serialization cost:
// Version 1
#[derive(Fory)]
struct DataV1 {
#[fory(id = 0)]
id: i64,
#[fory(id = 1)]
name: String,
}
// Version 2: Added new field
#[derive(Fory)]
struct DataV2 {
#[fory(id = 0)]
id: i64,
#[fory(id = 1)]
name: String,
#[fory(id = 2)]
email: Option<String>, // New nullable field
}
Data serialized with V1 can be deserialized with V2 (new field will be None).
Alternatively, field IDs can be omitted (field names will be used in metadata with larger overhead):
#[derive(Fory)]
struct Data {
id: i64,
name: String,
}
Default Values
- Nullable:
Option<T>,RcWeak<T>, andArcWeak<T>are nullable by default; all other types are non-nullable - Ref tracking:
Rc<T>,Arc<T>,RcWeak<T>, andArcWeak<T>enable ref tracking by default; all other types are disabled
You need to configure fields when:
- A field can be None (use
Option<T>) - A field needs reference tracking for shared/circular objects (use
ref = true) - Integer types need specific encoding for cross-language compatibility
- You want to reduce metadata size (use field IDs)
// Xlang mode: explicit configuration required
#[derive(Fory)]
struct User {
#[fory(id = 0)]
name: String, // Non-nullable by default
#[fory(id = 1)]
email: Option<String>, // Nullable (Option<T>)
#[fory(id = 2, ref = true)]
friend: Rc<User>, // Ref tracking (default for Rc)
}
Default Values Summary
| Type | Default Nullable | Default Ref Tracking |
|---|---|---|
Primitives, String | false | false |
Option<T> | true | false |
Rc<T>, Arc<T> | false | true |
RcWeak<T>, ArcWeak<T> | true | true |
Best Practices
- Configure field IDs: Recommended for compatible mode to reduce serialization cost
- Use
skipfor sensitive data: Passwords, tokens, internal state - Enable ref tracking for shared objects: When the same pointer appears multiple times
- Disable ref tracking for unique fields: Optimization when you know the field is unique
- Choose appropriate encoding:
varintfor small values,fixedfor full-range values - Keep IDs stable: Once assigned, don't change field IDs
Options Reference
| Option | Syntax | Description | Valid For |
|---|---|---|---|
id | id = N | Field tag ID to reduce metadata size | All fields |
skip | skip | Exclude field from serialization | All fields |
nullable | nullable or nullable = bool | Control null flag writing | All fields |
ref | ref or ref = bool | Control reference tracking | Rc, Arc, weak types |
encoding | encoding = "varint/fixed/tagged" | Integer encoding method | i32, u32, i64, u64 |
compress | compress or compress = bool | Shorthand for varint/fixed | i32, u32 |
Related Topics
- Basic Serialization - Getting started with Fory serialization
- Schema Evolution - Compatible mode and schema evolution
- Cross-Language - Interoperability with Java, C++, Go, Python