title: Schema Metadata sidebar_position: 7 id: schema_metadata 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
This page explains how to configure field-level metadata for serialization in Java.
Apache Fory™ provides field-level configuration through annotations:
@ForyField: Configure field metadata (id, dynamic)@Nullable: Mark a field type or nested type position as nullable@Ref: Enable field or nested-element reference tracking@Ignore: Exclude fields from serializationThis enables:
Use annotations on fields:
import org.apache.fory.annotation.ForyField; import org.apache.fory.annotation.Nullable; public class Person { @ForyField(id = 0) private String name; @ForyField(id = 1) private int age; @Nullable @ForyField(id = 2) private String nickname; }
@ForyField AnnotationUse @ForyField to configure field-level metadata:
public class User { @ForyField(id = 0) private long id; @ForyField(id = 1) private String name; @Nullable @ForyField(id = 2) private String email; @ForyField(id = 3) private List<@Ref User> friends; @ForyField(id = 4, dynamic = ForyField.Dynamic.TRUE) private Object data; }
| Parameter | Type | Default | Description |
|---|---|---|---|
id | int | -1 | Non-negative field tag ID, or no ID |
dynamic | Dynamic | AUTO | Control polymorphism for struct fields |
Use @Nullable on the field type or nested type position for nullable schema metadata and @Ref for reference tracking. @ForyField does not carry either setting.
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:
Recommendation: It is recommended to configure field IDs for compatible mode since it reduces serialization cost.
Notes:
-1 is ignored and 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)Use @Nullable for fields that can be null:
public class Record { // Nullable string field @Nullable @ForyField(id = 0) private String optionalName; // Nullable Integer field (boxed type) @Nullable @ForyField(id = 1) private Integer optionalCount; // Non-nullable field (default) @ForyField(id = 2) private String requiredName; }
Notes:
Integer, Long, etc.) that can be null should use @Nullable.@Ref)Enable reference tracking for fields that may be shared or circular:
public class RefOuter { // Both fields may point to the same inner object @Nullable @ForyField(id = 0) @Ref private RefInner inner1; @Nullable @ForyField(id = 1) @Ref private RefInner inner2; } public class CircularRef { @ForyField(id = 0) private String name; // Self-referencing field for circular references @Nullable @ForyField(id = 1) @Ref private CircularRef selfRef; }
Use Cases:
Notes:
@Ref do not use field-wrapper reference tracking@Ref when values are not shared or circular, so Fory can skip the reference flagControls 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 |
@IgnoreExclude 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 }
transientJava'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 }
Fory provides annotations to control integer encoding for cross-language compatibility. Integer schema annotations are Java type-use annotations. Put them on the field type, after any field modifiers and alongside @ForyField when both are present.
@Int32Type)import org.apache.fory.annotation.Int32Type; import org.apache.fory.config.Int32Encoding; public class MyStruct { // Variable-length encoding (default) - compact for small values private @Int32Type(encoding = Int32Encoding.VARINT) int compactId; // Fixed 4-byte encoding - consistent size private @Int32Type(encoding = Int32Encoding.FIXED) int fixedId; }
@Int64Type)import org.apache.fory.annotation.Int64Type; import org.apache.fory.config.Int64Encoding; public class MyStruct { // Variable-length encoding (default) private @Int64Type(encoding = Int64Encoding.VARINT) long compactId; // Fixed 8-byte encoding private @Int64Type(encoding = Int64Encoding.FIXED) long fixedTimestamp; // Tagged encoding (4 bytes for small values, 9 bytes otherwise) private @Int64Type(encoding = Int64Encoding.TAGGED) long taggedValue; }
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.Int32Encoding; import org.apache.fory.config.Int64Encoding; public class UnsignedStruct { // Unsigned 8-bit [0, 255] private @UInt8Type int flags; // Unsigned 16-bit [0, 65535] private @UInt16Type int port; // Unsigned 32-bit with varint encoding (default) private @UInt32Type(encoding = Int32Encoding.VARINT) long compactCount; // Unsigned 32-bit with fixed encoding private @UInt32Type(encoding = Int32Encoding.FIXED) long fixedCount; // Unsigned 64-bit with various encodings private @UInt64Type(encoding = Int64Encoding.VARINT) long varintU64; private @UInt64Type(encoding = Int64Encoding.FIXED) long fixedU64; private @UInt64Type(encoding = Int64Encoding.TAGGED) long taggedU64; }
| Annotation | Type ID | Encoding | Size |
|---|---|---|---|
@Int32Type(encoding = VARINT) | 5 | varint | 1-5 bytes |
@Int32Type(encoding = FIXED) | 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(encoding = VARINT) | 12 | varint | 1-5 bytes |
@UInt32Type(encoding = FIXED) | 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 performanceUnsigned Java scalar carriers are int/Integer for @UInt8Type and @UInt16Type, and long/Long for @UInt32Type and @UInt64Type. Annotating byte with @UInt8Type is invalid because Java byte cannot represent the unsigned range.
Integer annotations can also be applied to nested generic type arguments:
import java.util.List; import java.util.Map; import org.apache.fory.annotation.Int64Type; import org.apache.fory.annotation.UInt32Type; import org.apache.fory.config.Int32Encoding; import org.apache.fory.config.Int64Encoding; public class NestedStruct { private Map< @UInt32Type(encoding = Int32Encoding.FIXED) Long, List<@Int64Type(encoding = Int64Encoding.TAGGED) Long>> values; }
Specialized unsigned list carriers use the list<T> schema by default, so their element annotations are preserved in list metadata. Add @ArrayType only when the field should use dense array<T> schema.
Primitive unsigned arrays can use scalar element annotations for dense array<T> metadata:
import org.apache.fory.annotation.UInt32Type; public class IdBatch { private @UInt32Type int[] ids; }
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.Nullable; import org.apache.fory.annotation.UInt64Type; import org.apache.fory.config.Int64Encoding; 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 @Nullable @ForyField(id = 2) 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) private @UInt64Type(encoding = Int64Encoding.VARINT) long viewCount; // varint encoding @ForyField(id = 7) private @UInt64Type(encoding = Int64Encoding.FIXED) long fileSize; // fixed encoding @ForyField(id = 8) private @UInt64Type(encoding = Int64Encoding.TAGGED) long checksum; // tagged encoding // Reference-tracked field for shared/circular references @Nullable @ForyField(id = 9) @Ref 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); } }
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) private @Int32Type(encoding = Int32Encoding.VARINT) int intVar; @ForyField(id = 1) private @UInt64Type(encoding = Int64Encoding.FIXED) long longFixed; @ForyField(id = 2) private @UInt64Type(encoding = Int64Encoding.TAGGED) long longTagged; @Nullable @ForyField(id = 3) private String optionalValue; }
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; @Nullable @ForyField(id = 2) 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; }
Java enums are serialized by numeric tag in xlang mode. The default tag is the declaration ordinal. When an enum needs stable ids that do not depend on declaration order, annotate exactly one id source with @ForyEnumId, or annotate every enum constant with explicit tag values.
import org.apache.fory.annotation.ForyEnumId; enum Status { Unknown(10), Running(20), Finished(30); private final int id; Status(int id) { this.id = id; } @ForyEnumId public int getId() { return id; } }
Java also supports annotating one enum instance field with @ForyEnumId, or annotating every enum constant directly, such as @ForyEnumId(10) Unknown.
@ForyEnumId supports exactly three configuration styles:
getId().@ForyEnumId(10) Unknown.Validation rules:
value() at its default -1.@ForyEnumId.int.Lookup behavior:
@ForyEnumId, Fory writes the declaration ordinal.@ForyEnumId, Fory writes the configured stable numeric tag instead.Use serializeEnumByName(true) only for Java native-mode peers that should match enum constants by name instead of numeric tag:
Fory fory = Fory.builder() .withXlang(false) .serializeEnumByName(true) .build();
This runtime option does not change xlang enum encoding; xlang uses numeric enum tags. Prefer @ForyEnumId for cross-language payloads or any schema where numeric wire ids must stay stable.
Field configuration behaves differently depending on the serialization mode:
Native mode has relaxed default values for maximum compatibility:
String, boxed types, and time types)In native mode, you typically don't need to configure field annotations unless you want to:
// 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 has stricter default values due to type system differences between languages:
@RefIn xlang mode, you need to configure fields when:
@Nullable)@Ref)// Xlang mode: explicit configuration required for nullable/ref fields public class User { @ForyField(id = 0) private long id; @ForyField(id = 1) private String name; @Nullable @ForyField(id = 2) // Must declare @Nullable private String email; @Nullable @ForyField(id = 3) @Ref // Must declare @Ref for shared objects private User friend; }
| Option | Native Mode Default | Xlang Mode Default |
|---|---|---|
nullable | true (reference types) | false |
ref | true | false |
dynamic | true (non-final) | AUTO (concrete types are final) |
@Nullable for nullable fields: Required for fields that can be null@Ref when objects are shared or circular@Ignore or transient for sensitive data: Passwords, tokens, internal statevarint for small values, fixed for full-range values| Annotation | Description |
|---|---|
@ForyField(id = N) | Field tag ID to reduce metadata size |
@Nullable | Mark field or nested type as nullable |
@Ref | Enable reference tracking |
@ForyField(dynamic = ...) | Control polymorphism for struct fields |
@Ignore | Exclude field from serialization |
@Int32Type(encoding = ...) | 32-bit signed integer encoding |
@Int64Type(encoding = ...) | 64-bit signed integer encoding |
@UInt8Type | Unsigned 8-bit integer |
@UInt16Type | Unsigned 16-bit integer |
@UInt32Type(encoding = ...) | Unsigned 32-bit integer encoding |
@UInt64Type(encoding = ...) | Unsigned 64-bit integer encoding |