Godot 4 Save System: Save & Load Game Data With JSON

Saving game progress is something almost every game needs, and Godot 4 makes it straightforward with its built-in FileAccess and JSON classes. In this tutorial you will build a complete, reusable save-and-load system in GDScript — no plugins, no third-party libraries — that works across all export platforms in Godot 4.4 through the current 4.6.x stable release.

By the end you will have a SaveManager autoload that serializes a Dictionary of game data to a JSON file on disk and reads it back cleanly. You will also learn which Godot types need manual conversion before they can go into JSON, how to handle missing or corrupted save files without crashing, and a few patterns that prevent subtle bugs later.

Quick Answer

To save game data in Godot 4 with JSON: pack your state into a Dictionary, call JSON.stringify(data) to convert it to a string, then write it to disk with FileAccess.open(“user://save_game.json”, FileAccess.WRITE) and file.store_string(json_text). To load: confirm the file exists with FileAccess.file_exists(path), open it with FileAccess.READ, retrieve the text with file.get_as_text(), and parse it back into a Dictionary with JSON.parse_string(text). That is the entire pattern.

Setting Up a SaveManager Autoload

Create a new GDScript file called save_manager.gd and register it as an Autoload singleton under Project > Project Settings > Autoload. Name it SaveManager. This makes your save and load functions available from any node in the game via SaveManager.save_game() and SaveManager.load_game() without passing references around.

At the top of the script, declare two constants: const SAVE_PATH = “user://save_game.json” and const SAVE_VERSION = 1. The user:// prefix is critical — it resolves to a writable OS-specific directory on every platform (AppData on Windows, ~/.local/share on Linux, ~/Library/Application Support on macOS) and remains writable in exported builds, unlike res:// which becomes read-only after export. The version constant lets you detect and migrate old saves when your data structure changes in a future update.

You can find the exact path on disk during development by opening the Godot editor and going to Project > Open User Data Folder. This is also where you can manually delete a save file to test a fresh start.

Writing the Save Function

The save function collects your game state into a GDScript Dictionary and converts it to a JSON string. A minimal example: func save_game(player_health: int, level: int, gold: int) -> void: var data = {“version”: SAVE_VERSION, “player_health”: player_health, “level”: level, “gold”: gold}. Then convert and write it: var json_text = JSON.stringify(data, “\t”) and then var file = FileAccess.open(SAVE_PATH, FileAccess.WRITE) followed by if file: file.store_string(json_text). The second argument “\t” to JSON.stringify() adds tab indentation, making the saved file human-readable during development — you can remove it in a shipping build if you want a smaller file.

Godot 4 automatically closes the FileAccess object when it goes out of scope, so you do not need to call file.close() manually. The if file: guard before store_string() is important: FileAccess.open() returns null if the path is invalid or permissions are denied, and calling a method on null will crash the game.

Call SaveManager.save_game() at natural checkpoints — level transitions, after picking up an item, on the pause menu — or set up an auto-save using a Timer node that fires every few minutes.

Writing the Load Function

The load function must handle two failure cases gracefully: the save file does not exist yet (first launch), and the file exists but the JSON is unparsable (corruption or a leftover file from an old build). Start with an existence check: if not FileAccess.file_exists(SAVE_PATH): return get_default_save(). The get_default_save() function should return a Dictionary with the same keys as a fresh save so the rest of your game always gets a valid structure.

If the file exists, open it: var file = FileAccess.open(SAVE_PATH, FileAccess.READ) and read the full content with var text = file.get_as_text(). Parse with the static method JSON.parse_string(text), which returns the decoded Dictionary directly — or null on failure. Always null-check: var data = JSON.parse_string(text); if data == null: return get_default_save(). Then optionally check data.get(“version”, 0) against SAVE_VERSION and run any migration logic before returning data.

In your game scenes, call var save_data = SaveManager.load_game() on _ready() and apply the values — player_health = save_data[“player_health”], current_level = save_data[“level”], and so on.

Handling Godot Types JSON Cannot Store

JSON only understands strings, numbers, booleans, arrays, and objects (dictionaries). Godot-native types — Vector2, Vector3, Color, Rect2, Transform2D — have no JSON equivalent and must be converted manually. For a Vector2 position, store it as a sub-dictionary: {“x”: position.x, “y”: position.y} and rebuild on load with Vector2(data[“pos”][“x”], data[“pos”][“y”]). A Color can be saved as its hex string using color.to_html() and restored with Color.html(hex_string).

If you find yourself converting many complex Godot types, consider switching to binary serialization: FileAccess.store_var(data, true) saves any Variant including all Godot types, and file.get_var(true) reads it back. The downside is that the file is not human-readable. Alternatively, Godot’s ResourceSaver and ResourceLoader workflow handles all native types if you define your save state as a custom Resource subclass — a more structured approach for larger games.

Tips and Common Mistakes

Never write save files to res://. It works in the editor but silently fails in exported games — one of the most common Godot save-system bugs. Always use user://.

Always guard FileAccess.open() with a null check before calling any method on the returned object. A failed open returns null, not an error code.

Do not call file.close() in Godot 4 — the file is closed automatically when the FileAccess variable is freed. Calling close() is harmless but leads to copy-paste confusion when people follow Godot 3 tutorials.

Store a version number in every save file from day one. When you rename a key or restructure your data in a later build, the version field lets you write a migration function instead of breaking all existing saves.

For save files that unlock premium content or should not be trivially edited by players, use FileAccess.open_encrypted_with_pass(path, FileAccess.WRITE, passphrase). The API is identical to a regular FileAccess — you only change the open call — and Godot 4 handles the AES encryption internally.

Explore more: Game Development tutorials.

Godot 4 JSON Save System FAQs

Where are Godot 4 save files stored on disk?

The user:// path maps to %APPDATA%\Godot\app_userdata\[ProjectName] on Windows, ~/.local/share/godot/app_userdata/[ProjectName] on Linux, and ~/Library/Application Support/Godot/app_userdata/[ProjectName] on macOS. In the Godot editor you can open this folder directly via Project > Open User Data Folder.

Should I use JSON or a .tres Resource file for save data in Godot 4?

JSON is a good starting point for simple, portable data — scores, flags, numeric stats, and settings. Godot Resource files (.tres / .res) are better when your save data maps to a custom Resource subclass or contains many Godot-native types like Vector2 or Color, since ResourceSaver handles those without manual conversion. For most indie games, JSON is simpler to get started with and easy to debug in a text editor.

How do I implement multiple save slots in Godot 4?

Pass a slot index into your file path: var path = “user://save_slot_%d.json” % slot_index. Your save and load functions stay exactly the same — only the path argument changes per slot. Store a list of slot metadata (slot number, timestamp, player level) in a separate index file so you can show a save-slot selection screen without loading each full save file.

Build It With GTStudios

Need help shipping your app, game, or small-business tech? GTStudios builds web, apps, and games. See how GTStudios can help.

Photo: Gflare / CC0, via Wikimedia Commons.