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.
- 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:
- Serialize Java objects to JSON (object to string)
- Deserialize JSON to Java objects (string to object)
- 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
- 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
}
Gson doesn't have its own module-info.class, so it's treated as an automatic module. This means:
- You can't use
requireswith the module namecom.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.gsonstill 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() { ... }
}
user- A User object storing user informationalbums- A List of Album objects, each containing a list of Song objectssaveData()- Serializes the entire Database to JSONloadData()- 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!");
}
}
Gson gson = new Gson();
This creates a Gson object that will handle the serialization.
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.
System.out.println("Serialized JSON: " + json);
This prints the JSON to the console, which is useful for debugging.
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).
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.
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.
System.out.println("JSON written to database.json");
This prints a success message to the console.
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
}
Gson gson = new Gson();
Create a Gson object for deserialization.
reader = new FileReader("database.json");
This opens the database.json file for reading.
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.
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.
user = database.user; albums = database.albums;
This copies the user and albums from the deserialized Database object to the current Database instance (this).
We copy the data rather than using the deserialized object directly because:
- The
loadData()method is called on an existing Database instance (created inJukebox.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
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
}
]
}
]
}
- The outer object represents the Database
useris an object with ausernamefieldalbumsis an array of Album objects- Each Album has
name,artist,genre, andsongs songsis an array of Song objects- Each Song has
titleandlengthSeconds
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();
- Application Start:
database = new Database()creates a new instance - Data Loading:
database.loadData()loads existing data from database.json - Application Use: Controllers access and modify data through
Jukebox.database - Data Saving:
database.saveData()writes current state to database.json - 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
}
- 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:
- Run the Jukebox application
- Go to Admin Panel and add some albums and songs
- Close the application and reopen it
- Notice that your data is still there - it was saved to database.json
- Open database.json in a text editor and look at the JSON structure
- Open
Database.javaand examine thesaveData()andloadData()methods - Find where
loadData()is called inJukebox.java - Try manually editing database.json (carefully!) and reloading the application
Common Pitfalls
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
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.
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.
Gson can serialize most objects, but there are exceptions:
- Objects with circular references might cause infinite loops
- Objects with transient fields (marked with
transientkeyword) are not serialized - Objects that don't have no-arg constructors might cause issues
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
- Gson provides simple APIs for JSON serialization and deserialization
- Create a Gson instance with
new Gson() - Serialize with
gson.toJson(object)orgson.toJson(object, writer) - Deserialize with
gson.fromJson(json, Class)orgson.fromJson(reader, Class) - Use FileWriter/FileReader for file I/O
- Always close resources (preferably with try-with-resources)
- Handle FileNotFoundException for first-time runs
- Gson automatically handles complex object graphs
- Field names in JSON match field names in Java classes