title: Field Nullability sidebar_position: 40 id: field_nullability 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 Fory handles field nullability in cross-language (xlang) serialization mode.
In xlang mode, fields are non-nullable by default. This means:
The following types are nullable by default:
Optional<T> (Java, C++)Integer, Long, Double, etc.)*int32, *string, etc.)Option<T>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 |
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 presentThese 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:
-1 or -2 flag, no reference deduplication≥0) for previously seen objectsWhen 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.
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() .withXlang(true) .build(); fory.register(Person.class, "example.Person");
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")
use fory::{Fory, ForyStruct}; #[derive(ForyStruct)] 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 }
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(forygo.WithXlang(true)) fory.RegisterNamedStruct(Person{}, "example.Person")
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);
public class Config { @ForyField(nullable = true) String optionalSetting; // Explicitly nullable @ForyField(nullable = false) String requiredSetting; // Explicitly non-nullable (default) }
struct Config { std::optional<std::string> optional_setting; std::string required_setting; }; FORY_STRUCT(Config, (optional_setting, fory::F(1)), (required_setting, fory::F(2)) );
For nullable pointer carriers, opt in with .nullable():
struct ConfigRef { std::shared_ptr<std::string> optional_setting; std::shared_ptr<std::string> required_setting; }; FORY_STRUCT(ConfigRef, (optional_setting, fory::F(1).nullable()), (required_setting, fory::F(2)) );
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 |
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