Field Nullability
This page explains how Fory handles field nullability in cross-language (xlang) serialization mode.
Default Behavior
In xlang mode, fields are non-nullable by default. This means:
- Values must always be present (non-null)
- No null flag byte is written for the field
- Serialization is more compact
The following types are nullable by default:
Optional<T>(Java, C++)- Java boxed types (
Integer,Long,Double, etc.) - Go pointer types (
*int32,*string, etc.) - Rust
Option<T> - Python
Optional[T]
| Field Type | Default Nullable | Null Flag Written |
|---|---|---|
Primitives (int, bool, float, etc.) | No | No |
String | No | No |
List<T>, Map<K,V>, Set<T> | No | No |
| Custom structs | No | No |
| Enums | No | No |
Java boxed types (Integer, Long, etc.) | Yes | Yes |
Go pointer types (*int32, *string) | Yes | Yes |
Optional<T> / Option<T> | Yes | Yes |
Wire Format
The nullable flag controls whether a null flag byte is written before the field value:
Non-nullable field: [value data]
Nullable field: [null_flag] [value data if not null]
Where null_flag is:
-1(NULL_FLAG): Value is null-2(NOT_NULL_VALUE_FLAG): Value is present
Nullable vs Reference Tracking
These are related but distinct concepts:
| Concept | Purpose | Flag Values |
|---|---|---|
| Nullable | Allow null values for a field | -1 (null), -2 (not null) |
| Reference Tracking | Deduplicate shared object references | -1 (null), -2 (not null), ≥0 (ref ID) |
Key differences:
- Nullable only: Writes
-1or-2flag, no reference deduplication - Reference tracking: Extends nullable semantics with reference IDs (
≥0) for previously seen objects - Both use the same flag byte position—ref tracking is a superset of nullable
When refTracking=true, the null flag byte doubles as a ref flag:
ref_flag = -1 → null value
ref_flag = -2 → new object (first occurrence)
ref_flag >= 0 → reference to object at index ref_flag
For detailed reference tracking behavior, see Reference Tracking.
Language-Specific Examples
Java
public class Person {
// Non-nullable by default in xlang mode
String name; // Must not be null
int age; // Primitive, always non-nullable
List<String> tags; // Must not be null
// Explicitly nullable
@ForyField(nullable = true)
String nickname; // Can be null
// Optional wrapper - nullable by default
Optional<String> bio; // Can be empty/null
}
Fory fory = Fory.builder()
.withLanguage(Language.XLANG)
.build();
fory.register(Person.class, "example.Person");
Python
from dataclasses import dataclass
from typing import Optional, List
import pyfory
@dataclass
class Person:
# Non-nullable by default
name: str # Must have a value
age: pyfory.int32 # Primitive
tags: List[str] # Must not be None
# Optional makes it nullable
nickname: Optional[str] = None # Can be None
bio: Optional[str] = None # Can be None
fory = pyfory.Fory(xlang=True)
fory.register_type(Person, typename="example.Person")
Rust
use fory::Fory;
#[derive(Fory)]
#[tag("example.Person")]
struct Person {
// Non-nullable by default
name: String,
age: i32,
tags: Vec<String>,
// Option<T> is nullable
nickname: Option<String>, // Can be None
bio: Option<String>, // Can be None
}
Go
type Person struct {
// Non-nullable by default
Name string
Age int32
Tags []string
// Pointer types for nullable fields
Nickname *string // Can be nil
Bio *string // Can be nil
}
fory := forygo.NewFory()
fory.RegisterTagType("example.Person", Person{})
C++
struct Person {
// Non-nullable by default
std::string name;
int32_t age;
std::vector<std::string> tags;
// std::optional for nullable
std::optional<std::string> nickname;
std::optional<std::string> bio;
};
FORY_STRUCT(Person, name, age, tags, nickname, bio);
Customizing Nullability
Java: @ForyField Annotation
public class Config {
@ForyField(nullable = true)
String optionalSetting; // Explicitly nullable
@ForyField(nullable = false)
String requiredSetting; // Explicitly non-nullable (default)
}
C++: fory::field Wrapper
struct Config {
// Explicitly mark as nullable
fory::field<std::string, 1, fory::nullable<true>> optional_setting;
// Explicitly mark as non-nullable (default)
fory::field<std::string, 2, fory::nullable<false>> required_setting;
};
FORY_STRUCT(Config, optional_setting, required_setting);
Null Value Handling
When a non-nullable field receives a null value:
| Language | Behavior |
|---|---|
| Java | Throws NullPointerException or serialization error |
| Python | Raises TypeError or serialization error |
| Rust | Compile-time error (non-Option types can't be None) |
| Go | Zero value is used (empty string, 0, etc.) |
| C++ | Default-constructed value or undefined behavior |
Schema Compatibility
The nullable flag is part of the struct schema fingerprint. Changing a field's nullability is a breaking change that will cause schema version mismatch errors.
Schema A: { name: String (non-nullable) }
Schema B: { name: String (nullable) }
// These have different fingerprints and are incompatible
Best Practices
- Use non-nullable by default: Only make fields nullable when null is a valid semantic value
- Use Optional/Option wrappers: Instead of raw types with nullable annotation
- Be consistent across languages: Use the same nullability for corresponding fields
- Document nullable fields: Make it clear which fields can be null in your API
See Also
- Reference Tracking - Shared and circular reference handling
- Serialization - Basic cross-language serialization
- Type Mapping - Cross-language type mapping reference
- Xlang Specification - Binary protocol details