Field Configuration
This page explains how to configure field-level metadata for serialization in Java.
Overview
Apache Fory™ provides field-level configuration through annotations:
@ForyField: Configure field metadata (id, nullable, ref, dynamic)@Ignore: Exclude fields from serialization- Integer type annotations: Control integer encoding (varint, fixed, tagged, unsigned)
This enables:
- Tag IDs: Assign compact numeric IDs to reduce struct field meta size overhead for compatible mode
- Nullability: Control whether fields can be null
- Reference Tracking: Enable reference tracking for shared objects
- Field Skipping: Exclude fields from serialization
- Encoding Control: Specify how integers are encoded
- Polymorphism Control: Control type info writing for struct fields
Basic Syntax
Use annotations on fields:
import org.apache.fory.annotation.ForyField;
public class Person {
@ForyField(id = 0)
private String name;
@ForyField(id = 1)
private int age;
@ForyField(id = 2, nullable = true)
private String nickname;
}
The @ForyField Annotation
Use @ForyField to configure field-level metadata:
public class User {
@ForyField(id = 0)
private long id;
@ForyField(id = 1)
private String name;
@ForyField(id = 2, nullable = true)
private String email;
@ForyField(id = 3, ref = true)
private List<User> friends;
@ForyField(id = 4, dynamic = ForyField.Dynamic.TRUE)
private Object data;
}
Parameters
| Parameter | Type | Default | Description |
|---|---|---|---|
id | int | -1 | Field tag ID (-1 = use field name) |
nullable | boolean | false | Whether the field can be null |
ref | boolean | false | Enable reference tracking |
dynamic | Dynamic | AUTO | Control polymorphism for struct fields |
Field ID (id)
Assigns a numeric ID to a field to minimize struct field meta size overhead for compatible mode:
public class User {
@ForyField(id = 0)
private long id;
@ForyField(id = 1)
private String name;
@ForyField(id = 2)
private int age;
}
Benefits:
- Smaller serialized size (numeric IDs vs field names in metadata)
- Reduced struct field meta overhead
- 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 class
- IDs must be >= 0 (use -1 to use field name encoding, which is the default)
- If not specified, field name is used in metadata (larger overhead)
Without field IDs (field names used in metadata):
public class User {
private long id;
private String name;
}
Nullable Fields (nullable)
Use nullable = true for fields that can be null:
public class Record {
// Nullable string field
@ForyField(id = 0, nullable = true)
private String optionalName;
// Nullable Integer field (boxed type)
@ForyField(id = 1, nullable = true)
private Integer optionalCount;
// Non-nullable field (default)
@ForyField(id = 2)
private String requiredName;
}
Notes:
- Default is
nullable = false(non-nullable) - When
nullable = false, Fory skips writing the null flag (saves 1 byte) - Boxed types (
Integer,Long, etc.) that can be null should usenullable = true
Reference Tracking (ref)
Enable reference tracking for fields that may be shared or circular:
public class RefOuter {
// Both fields may point to the same inner object
@ForyField(id = 0, ref = true, nullable = true)
private RefInner inner1;
@ForyField(id = 1, ref = true, nullable = true)
private RefInner inner2;
}
public class CircularRef {
@ForyField(id = 0)
private String name;
// Self-referencing field for circular references
@ForyField(id = 1, ref = true, nullable = true)
private CircularRef selfRef;
}
Use Cases:
- Enable for fields that may be circular or shared
- When the same object is referenced from multiple fields
Notes:
- Default is
ref = false(no reference tracking) - When
ref = false, avoids IdentityMap overhead and skips ref tracking flag - Reference tracking only takes effect when global ref tracking is enabled
Dynamic (Polymorphism Control)
Controls polymorphism behavior for struct fields in cross-language serialization:
public class Container {
// AUTO: Interface/abstract types are dynamic, concrete types are not
@ForyField(id = 0, dynamic = ForyField.Dynamic.AUTO)
private Animal animal; // Interface - type info written
// FALSE: No type info written, uses declared type's serializer
@ForyField(id = 1, dynamic = ForyField.Dynamic.FALSE)
private Dog dog; // Concrete - no type info
// TRUE: Type info written to support runtime subtypes
@ForyField(id = 2, dynamic = ForyField.Dynamic.TRUE)
private Object data; // Force polymorphic
}
Options:
| Value | Description |
|---|---|
AUTO | Auto-detect: interface/abstract are dynamic, concrete types are not |
FALSE | No type info written, uses declared type's serializer directly |
TRUE | Type info written to support subtypes at runtime |
Skipping Fields
Using @Ignore
Exclude fields from serialization:
import org.apache.fory.annotation.Ignore;
public class User {
@ForyField(id = 0)
private long id;
@ForyField(id = 1)
private String name;
@Ignore
private String password; // Not serialized
@Ignore
private Object internalState; // Not serialized
}
Using transient
Java's transient keyword also excludes fields:
public class User {
@ForyField(id = 0)
private long id;
private transient String password; // Not serialized
private transient Object cache; // Not serialized
}
Integer Type Annotations
Fory provides annotations to control integer encoding for cross-language compatibility.
Signed 32-bit Integer (@Int32Type)
import org.apache.fory.annotation.Int32Type;
public class MyStruct {
// Variable-length encoding (default) - compact for small values
@Int32Type(compress = true)
private int compactId;
// Fixed 4-byte encoding - consistent size
@Int32Type(compress = false)
private int fixedId;
}
Signed 64-bit Integer (@Int64Type)
import org.apache.fory.annotation.Int64Type;
import org.apache.fory.config.LongEncoding;
public class MyStruct {
// Variable-length encoding (default)
@Int64Type(encoding = LongEncoding.VARINT)
private long compactId;
// Fixed 8-byte encoding
@Int64Type(encoding = LongEncoding.FIXED)
private long fixedTimestamp;
// Tagged encoding (4 bytes for small values, 9 bytes otherwise)
@Int64Type(encoding = LongEncoding.TAGGED)
private long taggedValue;
}
Unsigned Integers
import org.apache.fory.annotation.Uint8Type;
import org.apache.fory.annotation.Uint16Type;
import org.apache.fory.annotation.Uint32Type;
import org.apache.fory.annotation.Uint64Type;
import org.apache.fory.config.LongEncoding;
public class UnsignedStruct {
// Unsigned 8-bit [0, 255]
@Uint8Type
private short flags;
// Unsigned 16-bit [0, 65535]
@Uint16Type
private int port;
// Unsigned 32-bit with varint encoding (default)
@Uint32Type(compress = true)
private long compactCount;
// Unsigned 32-bit with fixed encoding
@Uint32Type(compress = false)
private long fixedCount;
// Unsigned 64-bit with various encodings
@Uint64Type(encoding = LongEncoding.VARINT)
private long varintU64;
@Uint64Type(encoding = LongEncoding.FIXED)
private long fixedU64;
@Uint64Type(encoding = LongEncoding.TAGGED)
private long taggedU64;
}
Encoding Summary
| Annotation | Type ID | Encoding | Size |
|---|---|---|---|
@Int32Type(compress = true) | 5 | varint | 1-5 bytes |
@Int32Type(compress = false) | 4 | fixed | 4 bytes |
@Int64Type(encoding = VARINT) | 7 | varint | 1-10 bytes |
@Int64Type(encoding = FIXED) | 6 | fixed | 8 bytes |
@Int64Type(encoding = TAGGED) | 8 | tagged | 4 or 9 bytes |
@Uint8Type | 9 | fixed | 1 byte |
@Uint16Type | 10 | fixed | 2 bytes |
@Uint32Type(compress = true) | 12 | varint | 1-5 bytes |
@Uint32Type(compress = false) | 11 | fixed | 4 bytes |
@Uint64Type(encoding = VARINT) | 14 | varint | 1-10 bytes |
@Uint64Type(encoding = FIXED) | 13 | fixed | 8 bytes |
@Uint64Type(encoding = TAGGED) | 15 | tagged | 4 or 9 bytes |
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: Good balance between size and performance- Unsigned types: For cross-language compatibility with Rust, Go, C++
Complete Example
import org.apache.fory.Fory;
import org.apache.fory.annotation.ForyField;
import org.apache.fory.annotation.Ignore;
import org.apache.fory.annotation.Int64Type;
import org.apache.fory.annotation.Uint64Type;
import org.apache.fory.config.LongEncoding;
import java.util.List;
import java.util.Map;
import java.util.Set;
public class Document {
// Fields with tag IDs (recommended for compatible mode)
@ForyField(id = 0)
private String title;
@ForyField(id = 1)
private int version;
// Nullable field
@ForyField(id = 2, nullable = true)
private String description;
// Collection fields
@ForyField(id = 3)
private List<String> tags;
@ForyField(id = 4)
private Map<String, String> metadata;
@ForyField(id = 5)
private Set<String> categories;
// Integer with different encodings
@ForyField(id = 6)
@Uint64Type(encoding = LongEncoding.VARINT)
private long viewCount; // varint encoding
@ForyField(id = 7)
@Uint64Type(encoding = LongEncoding.FIXED)
private long fileSize; // fixed encoding
@ForyField(id = 8)
@Uint64Type(encoding = LongEncoding.TAGGED)
private long checksum; // tagged encoding
// Reference-tracked field for shared/circular references
@ForyField(id = 9, ref = true, nullable = true)
private Document parent;
// Ignored field (not serialized)
private transient Object cache;
// Getters and setters...
}
// Usage
public class Main {
public static void main(String[] args) {
Fory fory = Fory.builder()
.withXlang(true)
.withCompatible(true)
.withRefTracking(true)
.build();
fory.register(Document.class, 100);
Document doc = new Document();
doc.setTitle("My Document");
doc.setVersion(1);
doc.setDescription("A sample document");
// Serialize
byte[] data = fory.serialize(doc);
// Deserialize
Document decoded = (Document) fory.deserialize(data);
}
}
Cross-Language Compatibility
When serializing data to be read by other languages (Python, Rust, C++, Go), use field IDs and matching type annotations:
public class CrossLangData {
// Use field IDs for cross-language compatibility
@ForyField(id = 0)
@Int32Type(compress = true)
private int intVar;
@ForyField(id = 1)
@Uint64Type(encoding = LongEncoding.FIXED)
private long longFixed;
@ForyField(id = 2)
@Uint64Type(encoding = LongEncoding.TAGGED)
private long longTagged;
@ForyField(id = 3, nullable = true)
private String optionalValue;
}
Schema Evolution
Compatible mode supports schema evolution. It is recommended to configure field IDs to reduce serialization cost:
// Version 1
public class DataV1 {
@ForyField(id = 0)
private long id;
@ForyField(id = 1)
private String name;
}
// Version 2: Added new field
public class DataV2 {
@ForyField(id = 0)
private long id;
@ForyField(id = 1)
private String name;
@ForyField(id = 2, nullable = true)
private String email; // New field
}
Data serialized with V1 can be deserialized with V2 (new field will be null).
Alternatively, field IDs can be omitted (field names will be used in metadata with larger overhead):
public class Data {
private long id;
private String name;
}
Native Mode vs Xlang Mode
Field configuration behaves differently depending on the serialization mode:
Native Mode (Java-only)
Native mode has relaxed default values for maximum compatibility:
- Nullable: Reference types are nullable by default
- Ref tracking: Enabled by default for object references (except
String, boxed types, and time types) - Polymorphism: All non-final classes support polymorphism by default
In native mode, you typically don't need to configure field annotations unless you want to:
- Reduce serialized size by using field IDs
- Optimize performance by disabling unnecessary ref tracking
- Control integer encoding for specific fields
// Native mode: works without any annotations
public class User {
private long id;
private String name;
private List<String> tags; // Nullable and ref-tracked by default
}
Xlang Mode (Cross-language)
Xlang mode has stricter default values due to type system differences between languages:
- Nullable: Fields are non-nullable by default (
nullable = false) - Ref tracking: Disabled by default (
ref = false) - Polymorphism: Concrete types are non-polymorphic by default
In xlang mode, you need to configure fields when:
- A field can be null (use
nullable = true) - 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 for nullable/ref fields
public class User {
@ForyField(id = 0)
private long id;
@ForyField(id = 1)
private String name;
@ForyField(id = 2, nullable = true) // Must declare nullable
private String email;
@ForyField(id = 3, ref = true, nullable = true) // Must declare ref for shared objects
private User friend;
}
Default Values Summary
| Option | Native Mode Default | Xlang Mode Default |
|---|---|---|
nullable | true (reference types) | false |
ref | true | false |
dynamic | true (non-final) | AUTO (concrete types are final) |
Best Practices
- Configure field IDs: Recommended for compatible mode to reduce serialization cost
- Use
nullable = truefor nullable fields: Required for fields that can be null - Enable ref tracking for shared objects: Use
ref = truewhen objects are shared or circular - Use
@Ignoreortransientfor sensitive data: Passwords, tokens, internal state - Choose appropriate encoding:
varintfor small values,fixedfor full-range values - Keep IDs stable: Once assigned, don't change field IDs
- Configure unsigned types for cross-language compatibility: When interoperating with unsigned numbers in Rust, Go, C++
Annotations Reference
| Annotation | Description |
|---|---|
@ForyField(id = N) | Field tag ID to reduce metadata size |
@ForyField(nullable = true) | Mark field as nullable |
@ForyField(ref = true) | Enable reference tracking |
@ForyField(dynamic = ...) | Control polymorphism for struct fields |
@Ignore | Exclude field from serialization |
@Int32Type(compress = ...) | 32-bit signed integer encoding |
@Int64Type(encoding = ...) | 64-bit signed integer encoding |
@Uint8Type | Unsigned 8-bit integer |
@Uint16Type | Unsigned 16-bit integer |
@Uint32Type(compress = ...) | Unsigned 32-bit integer encoding |
@Uint64Type(encoding = ...) | Unsigned 64-bit integer encoding |
Related Topics
- Basic Serialization - Getting started with Fory serialization
- Schema Evolution - Compatible mode and schema evolution
- Cross-Language - Interoperability with Python, Rust, C++, Go