Skip to main content

Welcome to a Grasp lesson

Create a free account to keep studying and create your own study path.

Lesson 68

Loading Game State from JSON

Recommended resources

Hello! Welcome to the next step in building our life simulation game.

In our last lesson, we successfully implemented the save_game function. You learned how to use the serde framework to serialize your entire GameState struct into a savegame.json file. This was a critical milestone in making your game world persistent.

Today, we'll complete the cycle. Our goal is to implement a load_game function to deserialize a JSON file and restore the entire game state. We will read the savegame.json file from the disk, parse its contents, and reconstruct the GameState struct in memory. By the end of this lesson, you will have a fully functional save/load system.

Let's dive in.

1. Deserialization: From File to Game State

Deserialization is the process of converting data from a stored format (like our JSON string) back into a complex, in-memory data structure (our Rust GameState struct). In the previous lesson, you already did the preparatory work for this by adding #[derive(Deserialize)] to all your relevant structs alongside #[derive(Serialize)]. This attribute tells serde how to reconstruct your structs from a data source.

Our task is to orchestrate this process:

  1. Read the contents of savegame.json into a string.
  2. Pass that string to serde_json to perform the deserialization.

The following video provides a concise and practical demonstration of this exact workflow.

Parse JSON with Rust & Serde

This short video, 'Parse JSON with Rust & Serde' by Josiah Parry, gets straight to the point, showing how to read a JSON file and deserialize its contents into Rust structs. This is the core pattern we will implement.

Watch from 02:00 to 02:43. Pay close attention to the two key function calls: std::fs::read_to_string to load the file's content and serde_json::from_str to parse the JSON data into a vector of structs.

As you saw in the video, the process is surprisingly straightforward, thanks to the power of serde and Rust's standard library.


2. Implementing the load_game Function

We'll implement load_game as a static method on the GameState struct. This is a function that's associated with the GameState type itself, rather than a specific instance of it. This is a logical choice because its job is to create a GameState instance from a file. The function signature will look like this: pub fn load_game(path: &Path) -> Result<Self, ...>. The Self type here refers to GameState.

Let's build the function step-by-step in your src/game_state.rs file.

Step 1: Reading the Save File

First, we need to read the savegame.json file into a string. The std::fs::read_to_string function is perfect for this. It takes a path, opens the file, reads its entire contents into a String, and closes it.

use std::fs;
use std::path::Path;

// ...

// Inside impl GameState { ... }

// Previous functions like new() and save_game()

/// Deserializes a game state from a JSON file.
pub fn load_game(path: &Path) -> Result<Self, Box<dyn std::error::Error>> {
    // 1. Read the file content into a string.
    let data = fs::read_to_string(path)?;

    // ... deserialization will go here ...

    // Temporary placeholder
    Err("Not implemented yet".into())
}

Step 2: Deserializing the JSON String

With the JSON data loaded into a string, we can now use serde_json::from_str to parse it. This function is the mirror image of to_string that we used for saving.

A crucial point here is that from_str is a generic function. It needs to know what type of data structure it should try to create from the JSON. We provide this information using a type annotation.

Parsing JSON into Strongly Typed Rust Data Structures with Serde
This image illustrates the core concept: `serde_json::from_str` takes a JSON string and, with the help of a type annotation (`let deserialized_product: Product`), reconstructs the corresponding Rust struct.

We can now complete our load_game function.

// In src/game_state.rs

use serde::{Serialize, Deserialize};
use std::fs;
use std::io::{BufWriter, Write};
use std::path::Path;

// ... other imports




#[derive(Serialize, Deserialize, Debug)] // Make sure you also have Debug for easy printing!
pub struct GameState {
    // ... all your fields
}

impl GameState {
    // ... new() and save_game() methods

    /// Deserializes a game state from a JSON file.
    pub fn load_game(path: &Path) -> Result<Self, Box<dyn std::error::Error>> {
        // 1. Read the entire file into a string.
        let data = fs::read_to_string(path)?;

        // 2. Deserialize the string into a GameState instance.
        // The type `GameState` is inferred from the function's return type `Result<Self, ...>`.
        let game_state: GameState = serde_json::from_str(&data)?;

        // 3. Return the successfully loaded game state.
        Ok(game_state)
    }
}

Note: Make sure you've added #[derive(Debug)] to your GameState struct and its child structs. This will allow you to easily print the loaded state for verification later.


3. Handling Deserialization Errors

The ? operator neatly handles potential errors from file reading or deserialization. But what do these errors look like? Deserialization can fail for several reasons:

  • The JSON is malformed (e.g., a missing comma or bracket).
  • The data types don't match (e.g., a number in the JSON where the struct expects a string).
  • A field is missing in the JSON that is not marked as optional in the struct.

Understanding these errors is key to debugging your save/load system.

Hitchhiker's Guide to JSON Data in Rust πŸ¦€ Serialize and Deserialize with Serde πŸ—ΊοΈ Rust Tutorial

Let's revisit the 'Hitchhiker's Guide' video to see a practical demonstration of deserialization errors.

Watch from 26:51 to 28:50. The presenter intentionally breaks the JSON data and shows how serde provides informative error messages, like 'invalid type: integer 123, expected a string'. This is what you would see if your savegame.json became corrupted.

As a forward-looking practice, you can make your save format more resilient to changes. If you add a new field to a struct in your game, old save files will fail to load because that field is missing. You can prevent this by providing a default value.

Decoding Data with Serde in Rust for Optimal Performance

This article on Serde explains how to handle missing fields gracefully using the #[serde(default)] attribute.

In the section 'Example 1: JSON Operations with serde_json,' find the Product struct definition. Notice the is_available field is decorated with #[serde(default)]. The explanation clarifies that this allows the field to be missing from the JSON, in which case it will be initialized with its type's default value.

This is a powerful technique for ensuring that your save files remain backward-compatible as your game evolves. You don't need to implement it for every field now, but keep it in mind for the future.


4. Testing the Full Cycle

It's time to test the complete save-and-load loop. We'll modify the main function from the previous lesson to load the game state we already saved.

  1. Open your main.rs file.
  2. Update the code to call GameState::load_game.
// in main.rs
use std::path::Path;
// Make sure to import your GameState struct and any other necessary structs
// e.g., use your_project_name::game_state::GameState;

fn main() {
    let save_path = Path::new("savegame.json");

    // First, let's ensure a save file exists. 
    // If you don't have one from the last lesson, uncomment the block below,
    // run `cargo run` once, and then comment it out again.
    /*
    {
        println!("Creating a new save file...");
        // Use a populated GameState if you have one, otherwise a new one is fine.
        let mut game_state = GameState::new(); 
        // You could add some characters or change values here to test.
        match game_state.save_game(save_path) {
            Ok(_) => println!("Initial save file created successfully."),
            Err(e) => eprintln!("Error creating initial save file: {}", e),
        }
    }
    */

    // Now, attempt to load the game state from the file.
    println!("\nAttempting to load game from {}...", save_path.display());
    match GameState::load_game(save_path) {
        Ok(loaded_state) => {
            println!("Game loaded successfully!");
            // Use the dbg! macro to print the entire loaded state for verification.
            // This requires you to have `#[derive(Debug)]` on GameState and its sub-structs.
            dbg!(loaded_state);
        }
        Err(e) => {
            eprintln!("Error loading game: {}", e);
        }
    }
}
  1. Run the program with cargo run.

If everything is set up correctly, you should see the "Game loaded successfully!" message, followed by a detailed printout of your GameState struct, with all the data restored from your savegame.json file.

Congratulations! You now have a robust, file-based persistence system for your game.


Conclusion

In this lesson, you completed the second half of your game's persistence layer. You can now save the dynamic state of your world to a file and load it back, picking up exactly where you left off.

Key Takeaways:

  • You learned that deserialization is the process of reconstructing in-memory objects from stored data.
  • You implemented a static GameState::load_game function using std::fs::read_to_string to read the save file and serde_json::from_str to parse the JSON data.
  • You now understand how serde uses type annotations to correctly deserialize data into complex, nested Rust structs.
  • You tested the full save/load cycle, proving that your game state can be successfully written to and read from disk.

Preview of the Next Lesson:
With the foundational save/load system in place, our next module marks a very exciting step: adding modding support. In the next lesson, we will integrate a Lua scripting engine (like mlua) into the Rust backend. This will lay the groundwork for a powerful API that will allow you, and future players, to extend the game with custom content, starting with new character traits.