Tutorial2 min read

Save files that survive your next update

Every update that changes your save format can wipe a player’s progress. A version number, one migration step per version and a safe write stop it from ever happening.

The first time you rename a field in your save data, JsonUtility doesn’t complain. It loads the old file, finds no field with the new name, and quietly leaves it at its default. The player opens your update and their progress is gone, and the reviews tell you about it.

The fix costs about fifty lines, and it has to be in place before the first release, because you can’t go back and add a version number to files that are already on players’ phones.

Put a version in every save

C#
[System.Serializable]
public class SaveData
{
    // Files written before versioning have no "version" field, so they load as 1.
    public int version = 1;
    public int coins;
    public int bestLevel;
    public string[] unlockedSkins = new string[0];
}

One migration step per version

Each step upgrades a save by exactly one version. A player who comes back after a year still walks through every step in order, 1 → 2 → 3, and you never have to reason about skipping versions.

C#
public static class SaveSystem
{
    public const int CurrentVersion = 3;

    static SaveData Migrate(SaveData data, string json)
    {
        if (data.version < 2)
        {
            // v2 renamed "level" to "bestLevel"; the old value only exists in the JSON.
            data.bestLevel = JsonUtility.FromJson<V1>(json).level;
        }
        if (data.version < 3)
        {
            // v3 added skins; everyone starts with the classic one.
            data.unlockedSkins = new[] { "classic" };
        }
        data.version = CurrentVersion;
        return data;
    }

    [System.Serializable] class V1 { public int level; }
}

Write to a temporary file first

If the game is killed halfway through writing — a flat battery, or the OS reclaiming memory while it’s in the background — a half-written file is a corrupted save. Write somewhere else, then swap the finished file into place and keep the previous one as a backup:

C#
public static void Save(SaveData data)
{
    string path = Path.Combine(Application.persistentDataPath, "save.json");
    string temp = path + ".tmp";
    File.WriteAllText(temp, JsonUtility.ToJson(data));

    if (File.Exists(path)) File.Replace(temp, path, path + ".bak");
    else File.Move(temp, path);
}

Load with a fallback

C#
public static SaveData Load()
{
    string path = Path.Combine(Application.persistentDataPath, "save.json");
    foreach (string file in new[] { path, path + ".bak" })
    {
        if (!File.Exists(file)) continue;
        try
        {
            string json = File.ReadAllText(file);
            SaveData data = JsonUtility.FromJson<SaveData>(json);
            if (data != null) return Migrate(data, json);
        }
        catch (System.Exception e)
        {
            Debug.LogWarning($"Skipping unreadable save {file}: {e.Message}");
        }
    }
    return new SaveData { version = CurrentVersion };
}

If the main file is damaged, the backup from the previous save loads instead: the player loses one session, not everything.

Keep a museum of old saves

Before each release, copy a real save file from every version you’ve shipped into a test folder, and load each one in an Editor test that checks the migrated values. It takes a minute, and it’s the only way to know an update is safe for players who skipped the last three.

  • C#
  • Save systems
  • Live games

Written by

Logical Programmer

Solo indie developer. I build games in Unity, ship them on Google Play, and document all of it.

Built with this package

Systems

440 sales

Save System Ultimate

JSON + binary save with AES-256 encryption, multi-slot, versioning, and cloud save hooks.

$19.99was $29.99Save $10

Details

Checkout

Online checkout opens soon

Need Save System Ultimate today? Email me and I’ll sort out payment and the download for you.

Save System Ultimate$19.99

Keep reading

All articles
  • Tutorial2 min read

    Object pooling in Unity, the built-in way

    Unity has shipped a generic object pool since 2021. How to use UnityEngine.Pool properly: prewarming, resetting state, and the one mistake that corrupts a pool.

    Read

  • Tutorial2 min read

    Enemy AI you can read at a glance

    A finite state machine in plain C#: patrol, chase and search as small classes, with every transition in one place. No behaviour tree required.

    Read

  • Game design2 min read

    Why your mobile joystick feels wrong

    Three numbers decide whether an on-screen joystick feels sharp or mushy: the dead zone, the travel radius and where the stick’s centre sits. How to tune each one.

    Read