| --- |
| 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: |
| |
| ```java |
| 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: |
| |
| ```java |
| 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: |
| |
| ```java |
| 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): |
| |
| ```java |
| public class User { |
| private long id; |
| private String name; |
| } |
| ``` |
| |
| ## Nullable Fields (`nullable`) |
| |
| Use `nullable = true` for fields that can be `null`: |
| |
| ```java |
| 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: |
| |
| ```java |
| 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: |
| |
| ```java |
| 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: |
| |
| ```java |
| 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: |
| |
| ```java |
| 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`) |
| |
| ```java |
| 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`) |
| |
| ```java |
| 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 |
| |
| ```java |
| 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 |
| |
| ```java |
| 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: |
| |
| ```java |
| 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: |
| |
| ```java |
| // 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): |
| |
| ```java |
| 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 |
| |
| ```java |
| // 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) |
| |
| ```java |
| // 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 |
| |
| 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 |
| |
| | 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](basic-serialization.md) - Getting started with Fory serialization |
| - [Schema Evolution](schema-evolution.md) - Compatible mode and schema evolution |
| - [Cross-Language](cross-language.md) - Interoperability with Python, Rust, C++, Go |