Quick answer: An @export var dict: Dictionary without a default value initializer is null until assigned. Always use @export var dict: Dictionary = {}. For complex or typed data, use a custom Resource subclass instead of a Dictionary — it serializes reliably and gives you type safety in the inspector.

You open the inspector, carefully add five entries to your exported Dictionary, save the scene, run the game, and get a null reference error on the first frame. You go back to the inspector — the entries are still there. You run again — still null. This is one of Godot 4’s most frustrating gotchas, and it has bitten developers at every experience level.

Why the Dictionary Is Null

In GDScript, variable declarations do not automatically create instances of complex types. When you write:

@export var enemy_stats: Dictionary

This declares a variable of type Dictionary with no default value. In Godot 4, uninitialized Dictionary variables default to an empty dictionary in most contexts, but the interaction with @export and scene serialization adds complications.

The problem typically manifests in one of these scenarios:

The Basic Fix: Always Initialize

The simplest and most important fix is to always provide a default value:

# CORRECT: Always initialize exported dictionaries
@export var enemy_stats: Dictionary = {}
@export var loot_table: Dictionary = {
    "common": 0.6,
    "rare": 0.3,
    "legendary": 0.1
}

With the = {} initializer, the dictionary is guaranteed to exist at runtime even if the inspector data fails to load. The inspector can still override it — the default is only used when no serialized value is present.

The Deeper Problem: Complex Value Types

The basic fix handles simple dictionaries with primitive keys and values. But many developers want to store structured data — enemy definitions, item properties, level configurations — in an exported dictionary. This is where things get unreliable.

Consider this:

# Attempting to map enemy names to their stat resources
@export var enemies: Dictionary = {}
# In the inspector, you add: "goblin" -> EnemyStats.tres
# At runtime, enemies["goblin"] may be null or the wrong resource

The inspector can display Resource references inside a dictionary, but serialization of nested Resources inside dictionaries has historically been fragile. The .tscn file format encodes dictionary values inline, and complex objects may not round-trip correctly through save/load cycles.

The Robust Solution: Custom Resource Classes

For any data more complex than simple key-value pairs with primitive types, a custom Resource class is far more reliable than a Dictionary:

# enemy_entry.gd
class_name EnemyEntry
extends Resource

@export var id: StringName = ""
@export var max_health: int = 100
@export var damage: int = 10
@export var speed: float = 50.0
@export var loot_table: Array[StringName] = []

Then in your main script, export an array of these resources:

# spawner.gd
@export var enemy_entries: Array[EnemyEntry] = []

var _enemy_map: Dictionary = {}

func _ready():
    # Build the lookup dictionary from the resource array
    for entry in enemy_entries:
        _enemy_map[entry.id] = entry

func get_enemy_stats(id: StringName) -> EnemyEntry:
    return _enemy_map.get(id)

This approach gives you several advantages over an exported Dictionary:

Verifying Serialization in the Scene File

If you suspect the inspector is not saving your dictionary data, open the .tscn file in a text editor and look for your property:

; What a correctly serialized dictionary looks like:
[node name="Spawner" type="Node2D"]
script = ExtResource("1_abc12")
enemy_stats = {
"goblin": 50,
"skeleton": 80,
"dragon": 500
}

If the property is missing entirely, or if the values show null where you expected Resource references, the inspector did not serialize your data correctly. This is your confirmation that you need to switch to a Resource-based approach.

The _ready() Guard Pattern

Even with a default value, you should still guard against null dictionaries in _ready() as a defensive measure. This is especially important for scenes that might be instantiated from code without going through the inspector:

@export var config: Dictionary = {}

func _ready():
    # Defensive initialization
    if config == null or config.is_empty():
        config = _default_config()
        push_warning("Config dictionary was empty, using defaults")

func _default_config() -> Dictionary:
    return {
        "spawn_rate": 2.0,
        "max_enemies": 10,
        "difficulty": "normal"
    }

The push_warning call is important — it tells you when the fallback is being used so you can investigate rather than silently running with defaults when real data should have been present.

“We had a level designer who spent an entire afternoon configuring enemy spawns in the inspector. When she ran the game, every room was empty. The dictionary export looked fine in the editor but the .tscn file had zero entries. Switching to an Array of Resource entries fixed it permanently.”

Related Issues

Exported variable serialization issues often appear alongside other Godot 4 quirks. See Fix Godot Viewport Mouse Position Wrong After Resize for another common coordinate-related surprise, and Bug Report Template for Game QA Testers for how to structure reports when your data-driven configuration is the source of the bug.

Tip: When switching from Dictionary exports to Resource arrays, you lose the old inspector data. Copy your dictionary values somewhere before refactoring — the .tscn file is plain text, so you can extract them from there.