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

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 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 TypeDefault NullableNull Flag Written
Primitives (int, bool, float, etc.)NoNo
StringNoNo
List<T>, Map<K,V>, Set<T>NoNo
Custom structsNoNo
EnumsNoNo
Java boxed types (Integer, Long, etc.)YesYes
Go pointer types (*int32, *string)YesYes
Optional<T> / Option<T>YesYes

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:

ConceptPurposeFlag Values
NullableAllow null values for a field-1 (null), -2 (not null)
Reference TrackingDeduplicate shared object references-1 (null), -2 (not null), ≥0 (ref ID)

Key differences:

  • Nullable only: Writes -1 or -2 flag, 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()
    .withXlang(true)
    .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, 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
}

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(forygo.WithXlang(true))
fory.RegisterNamedStruct(Person{}, "example.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_STRUCT Field Config

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))
);

Null Value Handling

When a non-nullable field receives a null value:

LanguageBehavior
JavaThrows NullPointerException or serialization error
PythonRaises TypeError or serialization error
RustCompile-time error (non-Option types can't be None)
GoZero 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

  1. Use non-nullable by default: Only make fields nullable when null is a valid semantic value
  2. Use Optional/Option wrappers: Instead of raw types with nullable annotation
  3. Be consistent across languages: Use the same nullability for corresponding fields
  4. Document nullable fields: Make it clear which fields can be null in your API

See Also