Using JsonUtility in Unity to Save and Load Game Data

Unity provides the class JsonUtility and static methods for working with JSON. These provide a way to “serialize” and “deserialize” to and from JSON based on simple data structures in C#. Like BinaryFormatter, the class JsonUtility can be combined with other classes like File to read and write files, but also comes with the same warning: files on a user’s system, because they can be edited by the user, system, or other processes, should not be considered trusted sources. Always verify all data read from files.

Working with File

The C# class File provides methods for accessing files without knowing if the files are locally or remotely saved. As the action of needing to read text files is a common one, C# provides two useful methods for working with them as part of File class: ReadAllText() and WriteAllText().

NoteFile is part of the System.IO namespace, a collection of classes and methods connected to working with input (“I”) and output (“O”). When creating a new Scripting Component in Unity, the namespace will need to be added.

Where are files in Unity?

While a Unity project is running, it provides a field called Application.persistentDataPath set with the current “persistent” directory. This is set when the project starts and can only be used as part of the Initialization System within Unity as part of messages such as the method Awake() or Start() within a Scripting Component.

JSON File Example

 Application.persistentDataPath + "/gamedata.json"

The field Application.persistentDataPath only contains a directory. To use a file, an additional filename is needed such as the above example where a “/” and the name of the file is used.

Does this file exist?

The first step in working with files should always be to check if they exist. Because users, systems, and processes can edit files, this verifies a file exists before an attempt to read or otherwise work with it is done. The class File also provides the method Exists() for checking if a specific files exists. When working with Application.persistentDataPath, this is a good first step to identifying if a file exists before attempting to read it (which would fail) or if it needs to be created for the first time.

// Save the full path to the file.
string saveFile = Application.persistentDataPath + "/gamedata.data";

// Does it exist?
if(File.Exists(saveFile))
{
  // File exists!
}
else
{
  // File does not exist.
  //
  // This could mean it was deleted or has not been created yet.
}

Working with GameDataManager Class

To help better organize classes, a new, named class called GameDataManager can be created to “manage” the data eventually to be used with JsonUtility and read and saved using FileStream. As the actions of “reading data” and “writing data” are separate actions, these should also be separate methods of the class.

// Add System.IO to work with files!
using System.IO;
using System.Collections;
using System.Collections.Generic;
using UnityEngine;

public class GameDataManager : MonoBehaviour
{
    // Create a field of this class for the file.
    string saveFile;

    void Awake()
    {
        // Update the field once the persistent path exists.
        saveFile = Application.persistentDataPath + "/gamedata.json";
    }

    public void readFile()
    {
        // Does the file exist?
        if (File.Exists(saveFile))
        {
            // Work with JSON
        }
    }

    public void writeFile()
    {
        // Work with JSON
    }
}

Reading and Writing Text Files

When working with files, the normal series of steps is to open a file, work with it, and then close it. The methods ReadAllText() and WriteAllText() collapse two of those steps. They either read or write to a file and handle the opening and closing internally.

// Add System.IO to work with files!
using System.IO;
using System.Collections;
using System.Collections.Generic;
using UnityEngine;

public class GameDataManager : MonoBehaviour
{
    // Create a field for the save file.
    string saveFile;

    void Awake()
    {
        // Update the path once the persistent path exists.
        saveFile = Application.persistentDataPath + "/gamedata.json";
    }

    public void readFile()
    {
        // Does the file exist?
        if (File.Exists(saveFile))
        {
            // Read the entire file and save its contents.
            string fileContents = File.ReadAllText(saveFile);
            
            // Work with JSON
        }
    }

    public void writeFile()
    {

        // Work with JSON

        // Write JSON to file.
        File.WriteAllText(saveFile, jsonString);
    }
}

Using GameData Class

It is often useful to create a class whose purpose is to hold a collection of values used by other classes or processes. A new class, GameData, will serve this purpose.

Note: A new C# script file can be created in the Project View through Create -> C# Script. Right-click on the file to rename it something other than its default name.

The new GameData class does not need to be complex, as it will only hold values.

public class GameData
{
    // Public lives
    public int lives;

    // Public highScore
    public int highScore;
}

System.Serializable

A class is a complex data structure. In order for it to be “serialized,” additional information is need to let Unity and C# know its fields should be serialized.

[System.Serializable]
public class GameData
{
    // Public lives
    public int lives;

    // Public highScore
    public int highScore;
}

Combining GameDataManager and GameData

The class GameData contains values and the class GameDataManager “manages” these values, working with File to read and write to a file.

// Add System.IO to work with files!
using System.IO;
using System.Collections;
using System.Collections.Generic;
using UnityEngine;

public class GameDataManager : MonoBehaviour
{
    // Create a field for the save file.
    string saveFile;

    // Create a GameData field.
    GameData gameData = new GameData();

    void Awake()
    {
        // Update the path once the persistent path exists.
        saveFile = Application.persistentDataPath + "/gamedata.json";
    }

    public void readFile()
    {
        // Does the file exist?
        if (File.Exists(saveFile))
        {
            // Read the entire file and save its contents.
            string fileContents = File.ReadAllText(saveFile);

            // Work with JSON
        }
    }

    public void writeFile()
    {
        // Work with JSON

        // Write JSON to file.
        File.WriteAllText(saveFile, jsonString);
    }
}

Using JsonUtility

The class JsonUtility handles the work of serializing and deserializing data into and out of a JSON format. It uses the ToJson() and FromJson() for these tasks. However, in order to read data and understand the use of values, FromJson() also needs a Type to know what it should be trying to read. This is where GameData comes into use with the method.

Code Fragment Showing JsonUtility.FromJson()

// Read the entire file and save its contents.
string fileContents = File.ReadAllText(saveFile);

// Deserialize the JSON data 
//  into a pattern matching the GameData class.
gameData = JsonUtility.FromJson<GameData>(fileContents);

Through combining File.ReadAllText() and File.WriteAllText() and FromJson() and ToJson(), data can be serialized to and from a JSON file.

// Add System.IO to work with files!
using System.IO;
using System.Collections;
using System.Collections.Generic;
using UnityEngine;

public class GameDataManager : MonoBehaviour
{
    // Create a field for the save file.
    string saveFile;

    // Create a GameData field.
    GameData gameData = new GameData();

    void Awake()
    {
        // Update the path once the persistent path exists.
        saveFile = Application.persistentDataPath + "/gamedata.json";
    }

    public void readFile()
    {
        // Does the file exist?
        if (File.Exists(saveFile))
        {
            // Read the entire file and save its contents.
            string fileContents = File.ReadAllText(saveFile);

            // Deserialize the JSON data 
            //  into a pattern matching the GameData class.
            gameData = JsonUtility.FromJson<GameData>(fileContents);
        }
    }

    public void writeFile()
    {
        // Serialize the object into JSON and save string.
        string jsonString = JsonUtility.ToJson(gameData);

        // Write JSON to file.
        File.WriteAllText(saveFile, jsonString);
    }
}

JSON Data as a Start, Not a Finish Line

The class JsonUtility serializes and deserializes JSON data. Because users can edit this data at any time when working with files, it should not be fully trusted. It can be a starting point to create data formats using other tools, but anything read from files should be verified for acceptable ranges of values.