Skip to main content
Version: dev

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:

TypeDefault Nullable
Option<T>true
RcWeak<T>, ArcWeak<T>true
All other typesfalse

Notes:

  • For Option<T>, RcWeak<T>, ArcWeak<T>, nullable defaults to true
  • For all other types, nullable defaults to false
  • Use nullable = false to 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:

TypeDefault Ref Tracking
Rc<T>, Arc<T>true
RcWeak<T>, ArcWeak<T>true
Option<Rc<T>>, Option<Arc<T>>true (inherited)
All other typesfalse

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:

TypeOptionsDefault
i32, u32varint, fixedvarint
i64, u64varint, fixed, taggedvarint

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:

  • compress or compress = true is equivalent to encoding = "varint"
  • compress = false is equivalent to encoding = "fixed"
  • If both compress and encoding are specified, they must not conflict

Type Classification

Fory classifies field types to determine default behavior:

Type ClassExamplesDefault NullableDefault Ref
Primitivei8, i32, f64, boolfalsefalse
OptionOption<T>truefalse
RcRc<T>falsetrue
ArcArc<T>falsetrue
RcWeakRcWeak<T> (fory type)truetrue
ArcWeakArcWeak<T> (fory type)truetrue
OtherString, Vec<T>, user typesfalsefalse

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>, and ArcWeak<T> are nullable by default; all other types are non-nullable
  • Ref tracking: Rc<T>, Arc<T>, RcWeak<T>, and ArcWeak<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

TypeDefault NullableDefault Ref Tracking
Primitives, Stringfalsefalse
Option<T>truefalse
Rc<T>, Arc<T>falsetrue
RcWeak<T>, ArcWeak<T>truetrue

Best Practices

  1. Configure field IDs: Recommended for compatible mode to reduce serialization cost
  2. Use skip for sensitive data: Passwords, tokens, internal state
  3. Enable ref tracking for shared objects: When the same pointer appears multiple times
  4. Disable ref tracking for unique fields: Optimization when you know the field is unique
  5. Choose appropriate encoding: varint for small values, fixed for full-range values
  6. Keep IDs stable: Once assigned, don't change field IDs

Options Reference

OptionSyntaxDescriptionValid For
idid = NField tag ID to reduce metadata sizeAll fields
skipskipExclude field from serializationAll fields
nullablenullable or nullable = boolControl null flag writingAll fields
refref or ref = boolControl reference trackingRc, Arc, weak types
encodingencoding = "varint/fixed/tagged"Integer encoding methodi32, u32, i64, u64
compresscompress or compress = boolShorthand for varint/fixedi32, u32