Field Type Meta
Field type meta configuration controls whether type information is written during serialization for struct fields. This is essential for supporting polymorphism where the actual runtime type may differ from the declared field type.
Overview
When serializing a struct field, Fory needs to determine whether to write type metadata:
- Static typing: Use the declared field type's serializer directly (no type info written)
- Dynamic typing: Write type information to support runtime subtypes
When Type Meta Is Needed
Type metadata is required when:
- Interface/abstract fields: The declared type is abstract, so concrete type must be recorded
- Polymorphic fields: The runtime type may be a subclass of the declared type
- Cross-language compatibility: When the receiver needs type information to deserialize correctly
Type metadata is NOT needed when:
- Final/concrete types: The declared type is final/sealed and cannot be subclassed
- Primitive types: Type is known at compile time
- Performance optimization: When you know the runtime type always matches the declared type
Language-Specific Configuration
Java
Java requires explicit configuration because concrete classes can be subclassed unless marked final.
Use the @ForyField annotation with the dynamic parameter:
import org.apache.fory.annotation.ForyField;
import org.apache.fory.annotation.ForyField.Dynamic;
public class Container {
// AUTO (default): Interface types write type info, concrete types don't
@ForyField(id = 0)
private Shape shape; // Interface - type info written
// FALSE: Never write type info (use declared type's serializer)
@ForyField(id = 1, dynamic = Dynamic.FALSE)
private Circle circle; // Always treated as Circle
// TRUE: Always write type info (support runtime subtypes)
@ForyField(id = 2, dynamic = Dynamic.TRUE)
private Shape concreteShape; // Type info written even if concrete
}
Dynamic Options:
| Value | Behavior |
|---|---|
AUTO | Interface/abstract types are dynamic, concrete are not |
FALSE | Never write type info, use declared type's serializer |
TRUE | Always write type info to support runtime subtypes |
Use Cases:
AUTO: Default behavior, suitable for most casesFALSE: Performance optimization when you know the exact typeTRUE: When a concrete field may hold subclass instances
C++
C++ uses the fory::dynamic<V> template tag or .dynamic(bool) builder method:
Using fory::field<> template:
#include "fory/serialization/fory.h"
// Abstract base class with pure virtual methods
struct Animal {
virtual ~Animal() = default;
virtual std::string speak() const = 0;
};
struct Zoo {
// Auto: type info written because Animal is polymorphic (std::is_polymorphic)
fory::field<std::shared_ptr<Animal>, 0, fory::nullable> animal;
// Force non-dynamic: skip type info even though Animal is polymorphic
fory::field<std::shared_ptr<Animal>, 1, fory::nullable, fory::dynamic<false>> fixed_animal;
// Force dynamic: write type info even for non-polymorphic types
fory::field<std::shared_ptr<Data>, 2, fory::dynamic<true>> polymorphic_data;
};
FORY_STRUCT(Zoo, animal, fixed_animal, polymorphic_data);
Using FORY_FIELD_CONFIG macro:
struct Zoo {
std::shared_ptr<Animal> animal;
std::shared_ptr<Animal> fixed_animal;
std::shared_ptr<Data> polymorphic_data;
};
FORY_STRUCT(Zoo, animal, fixed_animal, polymorphic_data);
FORY_FIELD_CONFIG(Zoo,
(animal, fory::F(0).nullable()), // Auto-detect polymorphism
(fixed_animal, fory::F(1).nullable().dynamic(false)), // Skip type info
(polymorphic_data, fory::F(2).dynamic(true)) // Force type info
);
Default Behavior: Fory auto-detects polymorphism via std::is_polymorphic<T>. Types with pure virtual methods are treated as dynamic by default.
Go and Rust
Go and Rust do not require explicit dynamic configuration because:
- Go: Interface types are inherently dynamic - Fory can determine from the type whether it's an interface
- Rust: Trait objects (
dyn Trait) are explicitly marked in the type system
The type system in these languages already indicates whether a field is polymorphic:
// Go: interface types are automatically dynamic
type Container struct {
Shape Shape // Interface - type info written automatically
Circle Circle // Concrete struct - no type info needed
}
// Rust: trait objects are explicitly marked
struct Container {
shape: Box<dyn Shape>, // Trait object - type info written automatically
circle: Circle, // Concrete type - no type info needed
}
Python
Use pyfory.field() with the dynamic parameter:
from dataclasses import dataclass
from abc import ABC, abstractmethod
import pyfory
class Shape(ABC):
@abstractmethod
def area(self) -> float:
pass
@dataclass
class Circle(Shape):
radius: float = 0.0
def area(self) -> float:
return 3.14159 * self.radius * self.radius
@dataclass
class Container:
# Abstract class: dynamic is always True (type info written)
shape: Shape = pyfory.field(id=0)
# Concrete type with explicit dynamic=True (force type info)
circle: Circle = pyfory.field(id=1, dynamic=True)
# Concrete type with explicit dynamic=False (skip type info)
fixed_circle: Circle = pyfory.field(id=2, dynamic=False)
Default Behavior:
| Mode | Abstract Class | Concrete Object Types | Numeric/str/time Types |
|---|---|---|---|
| Native mode | True | True | False |
| Xlang mode | True | False | False |
- Abstract classes:
dynamicis alwaysTrue(type info must be written) - Native mode:
dynamicdefaults toTruefor object types,Falsefor numeric/str/time types - Xlang mode:
dynamicdefaults toFalsefor concrete types
Default Behavior
| Language | Interface/Abstract Types | Concrete Types |
|---|---|---|
| Java | Dynamic (write type) | Static (no type) |
| C++ | Dynamic (virtual) | Static |
| Go | Dynamic (interface) | Static (struct) |
| Rust | Dynamic (dyn Trait) | Static |
| Python | Dynamic (all objects) | Dynamic |
Performance Considerations
Writing type metadata has overhead:
- Space: Type information adds bytes to serialized output
- Time: Type resolution during serialization/deserialization
Use dynamic = FALSE (Java) or dynamic(false) (C++) when:
- You're certain the runtime type matches the declared type
- Performance is critical and polymorphism is not needed
- The field type is effectively final
Cross-Language Compatibility
When serializing data for cross-language consumption:
- Use consistent type registration: Register types with the same ID across languages
- Prefer explicit configuration: Use
dynamic = TRUEwhen unsure about receiver's expectations - Document polymorphic fields: Make it clear which fields may contain subtypes
Example: Polymorphic Container
Java
public interface Animal {
String speak();
}
public class Dog implements Animal {
private String name;
@Override
public String speak() { return "Woof!"; }
}
public class Cat implements Animal {
private String name;
@Override
public String speak() { return "Meow!"; }
}
public class Zoo {
// Type info written because Animal is an interface
@ForyField(id = 0)
private Animal animal;
// Force type info for concrete type that may hold subtypes
@ForyField(id = 1, dynamic = Dynamic.TRUE)
private Dog maybeMixedBreed;
}
C++
// Abstract base class with pure virtual methods
class Animal {
public:
virtual std::string speak() const = 0;
virtual ~Animal() = default;
};
class Dog : public Animal {
public:
std::string name;
std::string speak() const override { return "Woof!"; }
};
struct Zoo {
std::shared_ptr<Animal> animal;
std::shared_ptr<Dog> maybe_mixed_breed;
};
FORY_STRUCT(Zoo, animal, maybe_mixed_breed);
FORY_FIELD_CONFIG(Zoo,
(animal, fory::F(0).nullable()), // Auto-detect (Animal is polymorphic)
(maybe_mixed_breed, fory::F(1).dynamic(true)) // Force dynamic for concrete type
);
Related Topics
- Field Nullability - Controlling null handling for fields
- Field Reference Tracking - Managing shared/circular references
- Type Mapping - Cross-language type compatibility