title: Field Configuration sidebar_position: 5 id: field_configuration 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 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

Related Topics