Saving and Loading with Gson

Data persistence is essential for any application that needs to save user data between sessions. The Jukebox project uses Google's Gson library to serialize and deserialize application data to/from JSON files. This cookbook section explains how Gson is used in the Database class for persistence.

What You'll Learn:
  • Setting up Gson library
  • JSON serialization (saving data)
  • JSON deserialization (loading data)
  • File I/O operations
  • Error handling for persistence
  • Gson with complex object graphs

What is Gson?

Gson is a Java library from Google that provides simple APIs to:

Why Gson?
  • Simple API: Easy to use with just a few lines of code
  • No Configuration: Works out of the box for most use cases
  • Automatic Mapping: Automatically maps between JSON and Java objects
  • Handles Complex Objects: Can serialize/deserialize entire object graphs
  • Human-Readable: JSON is both machine-readable and human-readable
Gson in the Jukebox Project:
  • Library: gson-2.14.0.jar in the lib/ folder
  • Module Dependency: requires com.google.gson; in module-info.java
  • Usage: Used in Database.java for saving and loading

Module Configuration

To use Gson in a JavaFX project, you need to:

  • Add Gson JAR to Classpath

    Make sure gson-2.14.0.jar is in your project's lib/ folder and added to your IDE's libraries.

  • Add Module Dependency

    In module-info.java:

    open module jukebox {
        requires javafx.graphics;
        requires javafx.controls;
        requires java.desktop;
        requires javafx.fxml;
        requires com.google.gson;  // Gson library
    }
  • About Automatic Modules:

    Gson doesn't have its own module-info.class, so it's treated as an automatic module. This means:

    • You can't use requires with the module name com.google.gson (this is not the actual module name)
    • Instead, the module system automatically creates a module name based on the JAR filename
    • However, using requires com.google.gson still works in most cases due to how the module system resolves dependencies

    The Database Class

    The Database class in the Jukebox project is the central data store that also handles persistence. Let's look at its structure:

    package jukebox.model;
    
    import com.google.gson.Gson;
    import java.io.*;
    import java.util.ArrayList;
    import java.util.List;
    
    public class Database {
        public User user = new User();
        public List<Album> albums = new ArrayList<>();
        
        public void saveData() { ... }
        public void loadData() { ... }
    }
    Database Class Structure:
    • user - A User object storing user information
    • albums - A List of Album objects, each containing a list of Song objects
    • saveData() - Serializes the entire Database to JSON
    • loadData() - Deserializes JSON back to a Database object

    Saving Data (Serialization)

    The saveData() method serializes the entire Database object to a JSON file:

    public void saveData() {
        // 1. Create a Gson instance - this handles JSON serialization
        Gson gson = new Gson();
        
        // 2. Convert this entire Database object to a JSON string
        // This includes all user data and all albums with their songs
        String json = gson.toJson(this);
        
        // 3. Print the JSON to console for debugging purposes
        System.out.println("Serialized JSON: " + json);
        
        // 4. Create a FileWriter to write to the database.json file
        FileWriter writer = null;
        try {
            // 5. Open the file for writing (creates or truncates existing file)
            writer = new FileWriter("database.json");
            
            // 6. Use Gson to write the JSON directly to the file
            // This is more efficient than writing the string we already created
            gson.toJson(this, writer);
            
            // 7. Flush the writer to ensure all data is written to disk
            writer.flush();
            
            // 8. Log success message
            System.out.println("JSON written to database.json");
        } catch (IOException e) {
            // 9. If we can't write to the file, print an error message
            // Common causes: file permissions, disk full, path doesn't exist
            System.out.println("couldnt write to file!");
        }
    }
  • Step 1: Create Gson Instance

    Gson gson = new Gson();

    This creates a Gson object that will handle the serialization.

  • Step 2: Convert to JSON String

    String json = gson.toJson(this);

    This converts the entire Database object (including all nested objects) to a JSON string. The this keyword refers to the current Database instance.

  • Step 3: Debug Output

    System.out.println("Serialized JSON: " + json);

    This prints the JSON to the console, which is useful for debugging.

  • Step 4-5: Create and Open FileWriter

    writer = new FileWriter("database.json");

    This creates a FileWriter that will write to database.json in the current working directory. If the file doesn't exist, it will be created. If it does exist, it will be truncated (emptied).

  • Step 6: Write JSON to File

    gson.toJson(this, writer);

    This writes the JSON directly to the file. This is more efficient than first creating a string and then writing it, as it streams the JSON directly to the file.

  • Step 7: Flush the Writer

    writer.flush();

    This ensures all data is written to disk. Without flush(), some data might be buffered and not actually written to the file yet.

  • Step 8: Log Success

    System.out.println("JSON written to database.json");

    This prints a success message to the console.

  • Step 9: Error Handling

    catch (IOException e)

    This catches IOExceptions that might occur (file permissions, disk full, etc.) and prints an error message.

  • Loading Data (Deserialization)

    The loadData() method deserializes JSON from a file back into a Database object:

    public void loadData() {
        // 1. Create a Gson instance for deserialization
        Gson gson = new Gson();
        FileReader reader = null;
        try {
            // 2. Try to open the database.json file for reading
            reader = new FileReader("database.json");
            
            // 3. Deserialize the JSON file into a Database object
            // Gson automatically maps JSON fields to Java object fields
            Database database = gson.fromJson(reader, Database.class);
            
            // 4. Copy the loaded data into this instance
            // This replaces our empty/default data with the persisted data
            user = database.user;
            albums = database.albums;
        } catch (FileNotFoundException e) {
            // 5. File doesn't exist yet - this is normal on first run
            System.out.println("couldnt read from file!, creating new one");
            
            // 6. Create a new empty database.json file
            // This will contain an empty user and empty albums list
            saveData();
        }
        // Note: Other IOExceptions are not caught here, they would propagate up
        // but in practice this method is called from main() so they'd crash the app
    }
  • Step 1: Create Gson Instance

    Gson gson = new Gson();

    Create a Gson object for deserialization.

  • Step 2: Open FileReader

    reader = new FileReader("database.json");

    This opens the database.json file for reading.

  • Step 3: Deserialize JSON

    Database database = gson.fromJson(reader, Database.class);

    This reads from the FileReader and deserializes the JSON into a new Database object. Gson automatically maps JSON fields to Java object fields based on their names.

    Field Name Matching:

    Gson matches JSON field names to Java field names by default. For example:

    JSON:      Java Field:
    "name"    -->  album.name
    "artist"  -->  album.artist
    "songs"   -->  album.songs

    If field names don't match, you can use @SerializedName annotation to specify the JSON field name.

  • Step 4: Copy Data

    user = database.user; albums = database.albums;

    This copies the user and albums from the deserialized Database object to the current Database instance (this).

    Why Copy?

    We copy the data rather than using the deserialized object directly because:

    • The loadData() method is called on an existing Database instance (created in Jukebox.main())
    • We want to replace the default/empty data with the loaded data
    • This maintains the reference to the original Database instance that's stored in Jukebox.database
  • Step 5-6: Handle Missing File

    catch (FileNotFoundException e)

    If the database.json file doesn't exist (which is normal on first run), we:

    • Print an error message
    • Call saveData() to create a new empty database.json file
  • Example JSON Output

    Here's what the serialized JSON might look like for a Database with one album and two songs:

    {
      "user": {
        "username": ""
      },
      "albums": [
        {
          "name": "The Dark Side of the Moon",
          "artist": "Pink Floyd",
          "genre": "ROCK",
          "songs": [
            {
              "title": "Speak to Me",
              "lengthSeconds": 90
            },
            {
              "title": "Breathe",
              "lengthSeconds": 163
            }
          ]
        }
      ]
    }
    JSON Structure:
    • The outer object represents the Database
    • user is an object with a username field
    • albums is an array of Album objects
    • Each Album has name, artist, genre, and songs
    • songs is an array of Song objects
    • Each Song has title and lengthSeconds

    How Database is Used in the Application

    The Database instance is created and managed in Jukebox.java:

    // In Jukebox.java
    public static Database database;
    
    public static void main(String[] args) {
        // Initialize the database - this holds all our application data
        database = new Database();
        
        // Load data from the JSON file into our database object
        database.loadData();
        
        // Launch the JavaFX application
        launch(args);
    }
    
    // When saving is needed:
    Jukebox.database.saveData();
    Database Lifecycle:
    1. Application Start: database = new Database() creates a new instance
    2. Data Loading: database.loadData() loads existing data from database.json
    3. Application Use: Controllers access and modify data through Jukebox.database
    4. Data Saving: database.saveData() writes current state to database.json
    5. Application Exit: Auto-save ensures changes are persisted when leaving Admin Panel

    Creating Your Own Gson Persistence

    Let's create a simple persistence system using Gson:

  • Step 1: Create a Simple Model Class

    package myapp.model;
    
    import java.util.ArrayList;
    import java.util.List;
    
    public class AppData {
        public String title = "My App Data";
        public List<String> items = new ArrayList<>();
        public int count = 0;
    }
  • Step 2: Create Save/Load Methods

    package myapp.model;
    
    import com.google.gson.Gson;
    import java.io.*;
    
    public class AppDataStore {
        private static final String FILENAME = "app-data.json";
        private AppData data = new AppData();
        
        public void save() {
            Gson gson = new Gson();
            try (FileWriter writer = new FileWriter(FILENAME)) {
                gson.toJson(data, writer);
                System.out.println("Saved to " + FILENAME);
            } catch (IOException e) {
                System.out.println("Error saving: " + e.getMessage());
            }
        }
        
        public void load() {
            Gson gson = new Gson();
            try (FileReader reader = new FileReader(FILENAME)) {
                data = gson.fromJson(reader, AppData.class);
                System.out.println("Loaded from " + FILENAME);
            } catch (FileNotFoundException e) {
                System.out.println("No existing data file, using defaults");
            } catch (IOException e) {
                System.out.println("Error loading: " + e.getMessage());
            }
        }
        
        public AppData getData() {
            return data;
        }
    }
  • Step 3: Use the Data Store

    // Initialize and load
    AppDataStore store = new AppDataStore();
    store.load();
    
    // Access data
    AppData appData = store.getData();
    appData.items.add("New Item");
    appData.count++;
    
    // Save changes
    store.save();
  • Step 4: Try/Catch with Resources (Improved)

    The Jukebox implementation doesn't use try-with-resources, which is a best practice. Here's the improved version:

    public void saveData() {
        Gson gson = new Gson();
        try (FileWriter writer = new FileWriter("database.json")) {
            gson.toJson(this, writer);
            writer.flush();
            System.out.println("JSON written to database.json");
        } catch (IOException e) {
            System.out.println("couldnt write to file!");
        }
        // FileWriter is automatically closed by try-with-resources
    }
    
    public void loadData() {
        Gson gson = new Gson();
        try (FileReader reader = new FileReader("database.json")) {
            Database database = gson.fromJson(reader, Database.class);
            user = database.user;
            albums = database.albums;
        } catch (FileNotFoundException e) {
            System.out.println("couldnt read from file!, creating new one");
            saveData();
        } catch (IOException e) {
            System.out.println("Error reading file: " + e.getMessage());
        }
        // FileReader is automatically closed by try-with-resources
    }
  • Benefits of Try-With-Resources:
    • Automatic Closing: Resources are automatically closed when the try block exits
    • Cleaner Code: No need for finally blocks to close resources
    • Safer: Prevents resource leaks

    Try This:

    1. Run the Jukebox application
    2. Go to Admin Panel and add some albums and songs
    3. Close the application and reopen it
    4. Notice that your data is still there - it was saved to database.json
    5. Open database.json in a text editor and look at the JSON structure
    6. Open Database.java and examine the saveData() and loadData() methods
    7. Find where loadData() is called in Jukebox.java
    8. Try manually editing database.json (carefully!) and reloading the application

    Common Pitfalls

    Pitfall 1: Forgetting to Add Gson to Module Path

    If you get ClassNotFoundException for Gson classes, make sure:

    • The gson-2.14.0.jar file is in your lib/ folder
    • It's added as a library in your IDE
    • It's in the module path when compiling/running from command line
    Pitfall 2: Not Handling FileNotFoundException

    Always handle the case where the file doesn't exist yet. In Jukebox, they create a new file with default data. Don't let this exception crash your application.

    Pitfall 3: Not Closing Resources

    In the Jukebox implementation, the FileWriter and FileReader are not closed in a finally block. While this often works, it's better to use try-with-resources to ensure resources are always closed.

    Pitfall 4: Serializing Non-Serializable Objects

    Gson can serialize most objects, but there are exceptions:

    • Objects with circular references might cause infinite loops
    • Objects with transient fields (marked with transient keyword) are not serialized
    • Objects that don't have no-arg constructors might cause issues
    Pitfall 5: Not Flushing the Writer

    While FileWriter will eventually flush its buffer, calling flush() explicitly ensures data is written to disk immediately. This is especially important for critical data.

    Key Takeaways

    Related Cookbook Sections