【Unity】Save & Load in Unity: Persisting Game Data with JSON and Serialization

Created: 2025-12-07Last updated: 2026-07-12

A save system that preserves player progress is essential for most games. Go beyond the limits of PlayerPrefs and learn how to save and load complex game data to files using JSON and Unity's JsonUtility class, with practical C# code examples.

Level, held items, current position, cleared stages—with PlayerPrefs and its "only three types: int/float/string," saving a game's entire progress quickly hits a wall. Have you ever gotten stuck at "how do I even save a List of items?"

The answer is serialization to the JSON format. Group the data you want to save into a single C# class, convert it to a JSON string with JsonUtility, and write it to a file—build this shape once, and you can add new save fields just by adding fields to the class.

  • Serialization: Converting a C# object (in-memory data) into a format that can be saved to a file (a JSON string).
  • Deserialization: Restoring a JSON string back into the original C# object.

This article covers the basic pattern for saving and loading game data as JSON files with Unity's built-in JsonUtility, all the way to wiring it into a title screen's "Continue" button.

Save & load concept. Clay game data (level, hearts, item bag) is converted into a scroll-shaped save file and stored in a vault, with a reverse arrow showing it being restored

What You'll Learn

  • Serialization / deserialization — the round trip between C# objects and JSON strings
  • [System.Serializable] and the public-fields-only rule
  • Saving and loading files with JsonUtility.ToJson/FromJson plus the File class
  • The right place to save — Application.persistentDataPath
  • JsonUtility's limitations (no Dictionary support, etc.) and workarounds

Sponsored

Step 1: Define the Data Class to Save

First, create a C# class that defines the data structure you want to save. The key point is to mark the class with the [System.Serializable] attribute. Without this attribute, JsonUtility cannot serialize the class.

Serialization round-trip diagram. A C# GameData object (level, HP, position, item list) is converted by ToJson into curly-brace JSON text and written to a file, with a bidirectional arrow showing FromJson restoring it in the opposite direction
using System.Collections.Generic;

// This attribute is required!
[System.Serializable]
public class GameData
{
    public int level;
    public float currentHealth;
    public Vector3 playerPosition;
    public List<string> inventoryItems;

    // Set default values in the constructor
    public GameData()
    {
        this.level = 1;
        this.currentHealth = 100f;
        this.playerPosition = Vector3.zero;
        this.inventoryItems = new List<string>();
    }
}

Things to keep in mind:

  • Only public fields are serialized. private fields and properties are ignored.
  • JsonUtility cannot directly serialize certain complex types such as multidimensional arrays and Dictionary. List is supported.
Diagram of which fields JsonUtility saves. Within a class marked with the System.Serializable attribute, public int level and public List items are saved, but private int secret and Dictionary data are not

Step 2: Serialize the Data to JSON and Save It

Next, store the current game state in a GameData instance, convert it to a JSON string with JsonUtility.ToJson(), and write it to a file.

using UnityEngine;
using System.IO; // Required for file operations

public class SaveLoadManager : MonoBehaviour
{
    private string saveFilePath;
    private GameData gameData;

    void Awake()
    {
        // Determine the save file path
        // Application.persistentDataPath points to a persistent directory
        // that is safe to write to on every platform
        saveFilePath = Path.Combine(Application.persistentDataPath, "gamedata.json");

        gameData = new GameData();
    }

    public void SaveGame()
    {
        // --- Copy the current game state into the gameData object here ---
        // Example:
        // gameData.level = FindFirstObjectByType<GameManager>().currentLevel;
        // gameData.playerPosition = FindFirstObjectByType<PlayerController>().transform.position;
        // ----------------------------------------------------------

        // Convert the GameData object into a JSON string
        // Passing true as the second argument pretty-prints the output for readability
        string json = JsonUtility.ToJson(gameData, true);

        // Write the JSON string to a file
        File.WriteAllText(saveFilePath, json);

        Debug.Log("Save successful! Path: " + saveFilePath);
    }
}

Application.persistentDataPath points to a safe location — such as a folder inside the user's documents directory — that survives app uninstalls, making it the ideal place for save data.

Diagram of the save file location. Avoid the game's own folder, which can be wiped by updates or reinstalls, and save to the safe, persistent folder pointed to by Application.persistentDataPath
Sponsored

Step 3: Load the JSON File and Deserialize It

When the game starts or the player presses a load button, read the saved JSON file and restore it into a GameData object with JsonUtility.FromJson().

// Continued from the SaveLoadManager class

public void LoadGame()
{
    // Check whether a save file exists
    if (File.Exists(saveFilePath))
    {
        // Read the JSON string from the file
        string json = File.ReadAllText(saveFilePath);

        // Restore a GameData object from the JSON string
        gameData = JsonUtility.FromJson<GameData>(json);

        // --- Apply the restored data to the game here ---
        // Example:
        // FindFirstObjectByType<GameManager>().currentLevel = gameData.level;
        // FindFirstObjectByType<PlayerController>().transform.position = gameData.playerPosition;
        // --------------------------------------------------

        Debug.Log("Load successful!");
    }
    else
    {
        Debug.LogWarning("Save file not found. Starting new game.");
        // Logic for starting a new game
        gameData = new GameData();
    }
}

The FromJson<T>() method creates an object of the specified type T and overwrites its fields based on the JSON data.

Practical: Building a Title Screen "Continue" Button

Saving at an RPG inn, auto-saving when you pass an action-game checkpoint, a roguelite's "suspend save"—the shape differs, but the first thing the player touches is the title screen's "Continue" button. Let's connect the save & load from Steps 1–3 to this classic UI.

First, decide "when to save." The three standard moments are below, and all of them just call SaveGame() internally.

Diagram of three save timings: passing a checkpoint, manual save from the menu, and auto-save at regular intervals

On the title screen side, the key is to toggle the "Continue" button's state based on whether a save file exists.

Continue button implementation diagram. File.Exists checks whether a save file exists; if it does, the button is clickable, and if not, it is grayed out and dimmed
using UnityEngine;
using UnityEngine.UI;
using System.IO;

public class TitleMenu : MonoBehaviour
{
    [SerializeField] private Button continueButton;
    private string saveFilePath;

    void Start()
    {
        saveFilePath = Path.Combine(Application.persistentDataPath, "gamedata.json");

        // Disable "Continue" if there's no save file
        continueButton.interactable = File.Exists(saveFilePath);
    }

    // "New Game" button: recreate the save and start fresh
    public void OnNewGame()
    {
        var newData = new GameData();   // Start from the constructor's default values
        File.WriteAllText(saveFilePath, JsonUtility.ToJson(newData, true));
        // → Transition to the game scene
    }

    // "Continue" button: load and start
    public void OnContinue()
    {
        string json = File.ReadAllText(saveFilePath);
        GameData data = JsonUtility.FromJson<GameData>(json);
        // → Pass data to a GameManager, etc., and transition to the game scene
    }
}

There are two key points.

  • Control "Continue" with interactable: If it's clickable when there's no save, File.ReadAllText will throw an exception downstream. Tying the File.Exists pre-check directly to the button's appearance (grayed out) is friendlier to the user.
  • Design the "hand-off" of the loaded data: The GameData restored by FromJson needs to be passed to the main game across scenes. The standard approach is to combine scene transitions with DontDestroyOnLoad (or a ScriptableObject).

Bonus: Good to Know for Later

  • Multiple save slots: Simply varying the file name — save_slot1.json and so on — turns this into a slot system. For a slot-selection UI, showing each file's last modified time (File.GetLastWriteTime) is a nice touch.
  • Dictionary workaround: JsonUtility cannot handle Dictionaries directly. The standard workarounds are to keep a separate List of keys and a List of values, or to use a List of a [Serializable] class that pairs each key with its value.
  • Backward compatibility of save data: If an update adds new fields, old save files won't contain them. FromJson leaves missing fields at their constructor defaults, so always give new fields safe initial values.
  • More capable libraries: If the type restrictions become too limiting, Newtonsoft Json.NET (an official Unity package is available) supports Dictionaries and properties. Start with JsonUtility and switch when you actually need to.
  • For settings only: Saving to a file is overkill for simple option values like volume. Be mindful of when to use the PlayerPrefs article instead.

Summary

A save & load system built on JsonUtility gives you far more flexible and extensible data management than PlayerPrefs.

  • Create a class that groups the data you want to save, and mark it with the [System.Serializable] attribute (only public fields are saved).
  • To save: convert the data into a JSON string with JsonUtility.ToJson() and write it to a file with File.WriteAllText().
  • To load: read it with File.ReadAllText() and restore it with JsonUtility.FromJson<T>().
  • Application.persistentDataPath is the safe, reliable save location.
  • On the title screen, toggle the "Continue" button's activeness with File.Exists.

Building on this foundation, you can implement more advanced features such as managing multiple save slots or encrypting save data to deter cheating.