Skip to main content
Version: dev

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

ParameterTypeDefaultDescription
idint-1Field tag ID (-1 = use field name)
nullablebooleanfalseWhether the field can be null
refbooleanfalseEnable reference tracking
dynamicDynamicAUTOControl 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 use nullable = 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:

ValueDescription
AUTOAuto-detect: interface/abstract are dynamic, concrete types are not
FALSENo type info written, uses declared type's serializer directly
TRUEType 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

AnnotationType IDEncodingSize
@Int32Type(compress = true)5varint1-5 bytes
@Int32Type(compress = false)4fixed4 bytes
@Int64Type(encoding = VARINT)7varint1-10 bytes
@Int64Type(encoding = FIXED)6fixed8 bytes
@Int64Type(encoding = TAGGED)8tagged4 or 9 bytes
@Uint8Type9fixed1 byte
@Uint16Type10fixed2 bytes
@Uint32Type(compress = true)12varint1-5 bytes
@Uint32Type(compress = false)11fixed4 bytes
@Uint64Type(encoding = VARINT)14varint1-10 bytes
@Uint64Type(encoding = FIXED)13fixed8 bytes
@Uint64Type(encoding = TAGGED)15tagged4 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

OptionNative Mode DefaultXlang Mode Default
nullabletrue (reference types)false
reftruefalse
dynamictrue (non-final)AUTO (concrete types are final)

Best Practices

  1. Configure field IDs: Recommended for compatible mode to reduce serialization cost
  2. Use nullable = true for nullable fields: Required for fields that can be null
  3. Enable ref tracking for shared objects: Use ref = true when objects are shared or circular
  4. Use @Ignore or transient for sensitive data: Passwords, tokens, internal state
  5. Choose appropriate encoding: varint for small values, fixed for full-range values
  6. Keep IDs stable: Once assigned, don't change field IDs
  7. Configure unsigned types for cross-language compatibility: When interoperating with unsigned numbers in Rust, Go, C++

Annotations Reference

AnnotationDescription
@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
@IgnoreExclude field from serialization
@Int32Type(compress = ...)32-bit signed integer encoding
@Int64Type(encoding = ...)64-bit signed integer encoding
@Uint8TypeUnsigned 8-bit integer
@Uint16TypeUnsigned 16-bit integer
@Uint32Type(compress = ...)Unsigned 32-bit integer encoding
@Uint64Type(encoding = ...)Unsigned 64-bit integer encoding