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:
- No default value + inspector edits not saving. You add entries in the inspector, but the scene file does not serialize them correctly. At runtime, the variable is an empty dictionary, not the one you configured.
- Typed dictionary keys or values. If your dictionary uses custom Resource types as values, the inspector may display them but fail to serialize the references into the
.tscnfile properly. - Scene inheritance issues. A child scene overrides the script but inherits the parent’s property values. If the parent never initialized the dictionary, the child gets null regardless of what you set in its own inspector.
- Script reload clearing values. During development, modifying the script can trigger a property reset. If the new version of the script has a different export signature, Godot may discard the previously saved dictionary data.
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:
- Type safety. Each field has a declared type. The inspector enforces it.
- Reliable serialization. Resources serialize predictably in both
.tscnand.tresformats. - Reusability. You can save an
EnemyEntryas a.tresfile and reference the same entry from multiple scenes. - Refactoring safety. Adding or renaming fields on the Resource class preserves existing data, unlike dictionary keys which are just strings.
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.