Using the Jackson library to persist my JavaFX ToDo List to JSON
Learn how to use Jackson with Eclipse Collections and Java Date/Time
Upgrading my JavaFX ToDoList application
In the third iteration of my TodoListApplication
, I added a ToDoCategory
enum with an emoji String
, and used it to populate a JavaFX ComboBox
.
At the end of the third blog in this series, I said I would explain how I would make the ToDoList persistent in my next blog. Here I am, finally, doing what I said I would. This is my first experiment with using the Jackson library with Eclipse Collections, Java Date Time, and Java records. I am learning as I go, and aiming to share that learning with you, the reader, at the same time it is happening for me. It is quite possible that someone will see what I have done here, and give me valuable feedback on how it can be improved.
Where do my ToDoItem instances sleep at night?
When I was first learning about Object-Oriented programming and was presented with the concept of “persistence”, the topic was called “Where do objects sleep at night?” Here in lies the age old problem of where does our data go when we turn off the power. So far my JavaFX ToDoList has only been working in memory, and the ToDoItem
instances disappear when the application is closed.
There are a lot of choices for persisting data. I decided to use the Jackson library to convert my MutableList
of ToDoItem
instances to JSON and save it to and read it from a file.
Jackson, Eclipse Collections and JSR 310
The Jackson JSON library for Java is an amazing library. You can find out more about it here.
You can also keep up to date on the latest Jackson news by following @cowtowncoder here on Medium.
I am using Eclipse Collections MutableList
to hold onto the ToDoItem
instances in my application. I am using the LocalDate
class from Java Date Time (JSR 310) which was included in Java 8.
In order to use Jackson to persist my to-do list, I needed to include the following dependencies in my Maven pom.xml
file.
<dependency>
<groupId>com.fasterxml.jackson.core</groupId>
<artifactId>jackson-databind</artifactId>
<version>${jackson.version}</version>
</dependency>
<dependency>
<groupId>com.fasterxml.jackson.datatype</groupId>
<artifactId>jackson-datatype-jsr310</artifactId>
<version>${jackson.version}</version>
</dependency>
<dependency>
<groupId>com.fasterxml.jackson.datatype</groupId>
<artifactId>jackson-datatype-eclipse-collections</artifactId>
<version>${jackson.version}</version>
</dependency>
I am using JavaFX 17.0.1
, Eclipse Collections 11.1.0
and Jackson 2.14.1
with Java 17
. I am using LocalDate
from Java Date Time so needed to include the additional Jackson library (jackson-datatype-jsr310
) to support LocalDate
serialization as well.
<javafx.version>17.0.1</javafx.version>
<eclipse-collections.version>11.1.0</eclipse-collections.version>
<jackson.version>2.14.1</jackson.version>
Working with JPMS
The skeleton JavaFX project that IntelliJ generated for me was setup to use JPMS. I’ve never explicitly used JPMS with a Java application before. It caused me a few headaches, but I was able to successfully get things into a working order.
Here is the contents of the module-info.java
file that sits in the root of my Java source.
module example.todolist {
requires javafx.controls;
requires javafx.fxml;
requires org.eclipse.collections.api;
requires org.eclipse.collections.impl;
requires com.fasterxml.jackson.annotation;
requires com.fasterxml.jackson.core;
requires com.fasterxml.jackson.databind;
requires com.fasterxml.jackson.datatype.jsr310;
requires jackson.datatype.eclipse.collections;
opens example.todolist;
}
The thing that threw me for a loop was the inconsistency of the module name for jackson.datatype.eclipse.collections
. I had to go to the Jackson GitHub site to find out the name of the module.
Unfortunately, this followed a different module naming convention than the module for Google Guava, which is contained in same project.
It looks like Guava and HPPC follow one JPMS module naming convention, and Eclipse Collections and PCollections follow another in jackson-datatypes-collections.
Making changes to ToDoItem for Json serialization
I added the following annotations to my ToDoItem
record to get it to work with Jackson. It’s possible I don’t need some of these annotations, but this is definitely working (and is consistent), so sharing it here as is. If it can be simplified, please let me know in the comments.
docendo discimus
public record ToDoItem(
@JsonProperty String name,
@JsonProperty ToDoCategory category,
@JsonProperty LocalDate date)
{
@JsonIgnore
public String getCategory()
{
return this.category.getEmoji();
}
@JsonIgnore
public String getName()
{
return this.name;
}
@JsonIgnore
public String getDate()
{
return this.date.toString();
}
}
Update (12/6): I have updated the annotations to using JsonProperty instead of JsonSerialize, and removed the special handling for LocalDate.
I want to persist the data in the record, not the values returned from the getters. I realize I am mixing both my data and table presentation logic here. This is purely out of convenience. As I mentioned in my previous blogs, I used getters so my ToDoItem
record could work with the JavaFX TableView
. using PropertyValueFactory
definitions in the todolist-view.fxml
file.
Reading the Json file into an Eclipse Collections MutableList
Here’s the code I wrote to read the Json file into the MutableList
I use in the TodoListController
.
private MutableList<ToDoItem> readToDoListFromFile()
{
ObjectMapper mapper = this.getObjectMapper();
MutableList<ToDoItem> list = null;
try
{
list = mapper.readValue(
Paths.get("todolist.json").toFile(),
new TypeReference<MutableList<ToDoItem>>() {});
return list;
}
catch (IOException e)
{
System.out.println(e);
}
return Lists.mutable.empty();
}
I followed the instructions we have linked from the Eclipse Collections GitHub README for Serializing Eclipse Collections with Jackson. In the case the file does not exist (or some other exception happens), an empty MutableList
instance will be returned.
I created a getObjectMapper
method which always loads the two Jackson modules I need. I could probably hold onto the ObjectMapper
instance somewhere, but it’s working ok for now simply creating it as needed at startup and shutdown to read and write the file.
private ObjectMapper getObjectMapper()
{
ObjectMapper mapper = new ObjectMapper()
.registerModule(new EclipseCollectionsModule())
.registerModule(new JavaTimeModule());
return mapper;
}
Writing the the Json file from the MutableList
The code to write the Json file using Jackson is almost as simple as the code to read the Json file.
public void writeToDoListToFile()
{
ObjectMapper mapper = this.getObjectMapper();
MutableList<ToDoItem> list =
this.todoList.getItems().stream()
.collect(Collectors2.toList());
try
{
mapper.writeValue(
Paths.get("todolist.json").toFile(),
list);
}
catch (IOException e)
{
throw new RuntimeException(e);
}
}
The one issue I had is that my MutableList
is wrapped in an ObservableList
that is held on to in the TableView
in the todoList
variable. I access the ObservableList
by calling getItems
. I can’t get to the MutableList
directly through the ObservableList
. I have some ideas how I can improve this in later iterations (without simply holding onto a direct reference to the MutableList
), but for now I am taking the simple way out and converting the ObservableList
to a MutableList
using Java Streams and the Collectors2
utility from Eclipse Collections.
My exception handling could be improved, but is ok for this first iteration of persistence.
Using Jackson worked out well
In the end, it did not take a lot of code to persist and reify my MutableList of ToDoItem using Json. The Jackson library worked as well as I had hoped, with the minor package naming issue I ran into with JPMS modules.
This is what the output of the json file looks like (after formatting with IntelliJ).
[
{
"name": "Write ToDoList Persistence Blog",
"category": "RELAX",
"date": [2022, 12, 4]
},
{
"name": "Eat Dinner",
"category": "EAT",
"date": [2022, 12, 4]
}
]
JavaFX Lifecycle Events
I wanted the MutableList
of ToDoItem
to load automatically at app startup, and to be saved automatically when the window is closed and the application is shutdown. I will explain now how I was able to get this to work.
Loading the List of ToDoItem
I added the code to read in the JSON file to the initialize
method of the TodoListController
.
@FXML
protected void initialize()
{
MutableList<ToDoItem> items = this.readToDoListFromFile();
ObservableList<ToDoItem> list = FXCollections.observableList(items);
this.todoList.setColumnResizePolicy(TableView.CONSTRAINED_RESIZE_POLICY);
this.todoList.setItems(list);
ObservableList<ToDoCategory> categories =
FXCollections.observableList(
Lists.mutable.with(ToDoCategory.values()));
this.todoCategory.setItems(categories);
}
This code gets executed as the window is opening. Once the MutableList
is read in from the file using Jackson, it is set into the items List in the todoList
with a wrapped ObservableList
.
Saving the List of ToDoItem
This was the trickiest part for me. I knew instinctively that I should be able to hook some callback into the close event for the window. So I did some Googling and found some answer on how to do this. This required me to make some changes to my TodoListApplication
class.
I modified the start
method that IntelliJ initially generated for me as follows.
@Override
public void start(Stage stage) throws IOException
{
FXMLLoader fxmlLoader = new FXMLLoader();
Parent root = fxmlLoader.load(TodoListApplication.class.getResourceAsStream("todolist-view.fxml"));
TodoListController controller = fxmlLoader.getController();
Scene scene = new Scene(root, 640, 480);
stage.setTitle("Todo List");
stage.setScene(scene);
stage.show();
stage.setOnCloseRequest(event -> {
controller.writeToDoListToFile();
Platform.exit();
System.exit(0);
});
}
I needed to change the code around FXMLLoader
in order to get a handle to the TodoListController
so I could hook the Stage
setOnCloseRequest
event into calling the controllers writeToDoListToFile
method. As a byproduct of this I learned how you can get access to the controller associated to a view through the view itself. I still have to wrap my head around this completely, but the code at least works.
Oops, I need an error handling message
As I was playing around with my to-do list, I caused an exception to happen. If I left the date or anything else empty, I got a dreaded NullPointerException
. Yuck. I wanted to warn when an input is invalid with an error message. I discovered a simple approach using the Alert
class.
I refactored the onAddButtonClick
method in the TodoListController
class as follows so an error message will be displayed with any null inputs.
@FXML
protected void onAddButtonClick()
{
String text = this.todoItem.getText();
ToDoCategory category = this.todoCategory.getValue();
LocalDate date = this.todoDate.getValue();
if (text == null || category == null || date == null)
{
this.displayInvalidInputMessage();
}
else
{
this.createAndAddToDoItem(text, category, date);
}
}
private void displayInvalidInputMessage()
{
Alert errorAlert = new Alert(Alert.AlertType.ERROR);
errorAlert.setHeaderText("Invalid input");
errorAlert.setContentText("Text, category and date must all be specified.");
errorAlert.showAndWait();
}
If you don’t specify the text, category or date the following error message pops up now.
My Final ToDoItem for today
Hopefully I can get this published before midnight.
The Full Source
I will upload the source code for my to-do list experiment up to a GitHub project eventually. For now, the source code for my TodoListApplication
is contained in the following four files.
TodoListApplication.java
package example.todolist;
import java.io.IOException;
import javafx.application.Application;
import javafx.application.Platform;
import javafx.fxml.FXMLLoader;
import javafx.scene.Parent;
import javafx.scene.Scene;
import javafx.stage.Stage;
public class TodoListApplication extends Application
{
@Override
public void start(Stage stage) throws IOException
{
FXMLLoader fxmlLoader = new FXMLLoader();
Parent root = fxmlLoader.load(TodoListApplication.class.getResourceAsStream("todolist-view.fxml"));
TodoListController controller = fxmlLoader.getController();
Scene scene = new Scene(root, 640, 480);
stage.setTitle("Todo List");
stage.setScene(scene);
stage.show();
stage.setOnCloseRequest(event -> {
controller.writeToDoListToFile();
Platform.exit();
System.exit(0);
});
}
public static void main(String[] args)
{
TodoListApplication.launch();
}
}
todolist-view.fxml
<?xml version="1.0" encoding="UTF-8"?>
<?import javafx.geometry.Insets?>
<?import javafx.scene.control.Button?>
<?import javafx.scene.control.cell.PropertyValueFactory?>
<?import javafx.scene.control.ComboBox?>
<?import javafx.scene.control.DatePicker?>
<?import javafx.scene.control.Label?>
<?import javafx.scene.control.TableColumn?>
<?import javafx.scene.control.TableView?>
<?import javafx.scene.control.TextField?>
<?import javafx.scene.layout.HBox?>
<?import javafx.scene.layout.VBox?>
<VBox alignment="CENTER" spacing="20.0" xmlns:fx="http://javafx.com/fxml"
fx:controller="example.todolist.TodoListController">
<padding>
<Insets bottom="20.0" left="20.0" right="20.0" top="20.0" />
</padding>
<HBox id="HBox1" alignment="CENTER_LEFT" spacing="5.0">
<Label text="Item: " />
<TextField fx:id="todoItem" />
<Label text="Category: " />
<ComboBox fx:id="todoCategory" />
<Label text="Date: " />
<DatePicker fx:id="todoDate" />
</HBox>
<TableView fx:id="todoList">
<columns>
<TableColumn text="Name" minWidth="75.0" sortable="true">
<cellValueFactory>
<PropertyValueFactory property="name" />
</cellValueFactory>
</TableColumn>
<TableColumn text="Category" minWidth="50.0" sortable="true">
<cellValueFactory>
<PropertyValueFactory property="category" />
</cellValueFactory>
</TableColumn>
<TableColumn text="Date" minWidth="50.0" sortable="true">
<cellValueFactory>
<PropertyValueFactory property="date" />
</cellValueFactory>
</TableColumn>
</columns>
</TableView>
<HBox id="HBox2" alignment="CENTER" spacing="5.0">
<Button text="Add" onAction="#onAddButtonClick" alignment="BOTTOM_LEFT" />
<Button text="Remove" onAction="#onRemoveButtonClick" alignment="BOTTOM_RIGHT" />
</HBox>
</VBox>
TodoListController.java
(contains ToDoItem
and ToDoCategory
classes)
package example.todolist;
import java.io.IOException;
import java.nio.file.Paths;
import java.time.LocalDate;
import com.fasterxml.jackson.annotation.JsonIgnore;
import com.fasterxml.jackson.core.type.TypeReference;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.fasterxml.jackson.databind.annotation.JsonSerialize;
import com.fasterxml.jackson.datatype.eclipsecollections.EclipseCollectionsModule;
import com.fasterxml.jackson.datatype.jsr310.JavaTimeModule;
import com.fasterxml.jackson.datatype.jsr310.ser.LocalDateSerializer;
import javafx.collections.FXCollections;
import javafx.collections.ObservableList;
import javafx.fxml.FXML;
import javafx.scene.control.Alert;
import javafx.scene.control.ComboBox;
import javafx.scene.control.DatePicker;
import javafx.scene.control.TableView;
import javafx.scene.control.TextField;
import org.eclipse.collections.api.factory.Lists;
import org.eclipse.collections.api.list.MutableList;
import org.eclipse.collections.impl.collector.Collectors2;
public class TodoListController
{
@FXML
private TextField todoItem;
@FXML
public ComboBox<ToDoCategory> todoCategory;
@FXML
private DatePicker todoDate;
@FXML
private TableView<ToDoItem> todoList;
@FXML
protected void initialize()
{
MutableList<ToDoItem> items = this.readToDoListFromFile();
ObservableList<ToDoItem> list = FXCollections.observableList(items);
this.todoList.setColumnResizePolicy(TableView.CONSTRAINED_RESIZE_POLICY);
this.todoList.setItems(list);
ObservableList<ToDoCategory> categories =
FXCollections.observableList(
Lists.mutable.with(ToDoCategory.values()));
this.todoCategory.setItems(categories);
}
@FXML
protected void onAddButtonClick()
{
String text = this.todoItem.getText();
ToDoCategory category = this.todoCategory.getValue();
LocalDate date = this.todoDate.getValue();
if (text == null || category == null || date == null)
{
this.displayInvalidInputMessage();
}
else
{
this.createAndAddToDoItem(text, category, date);
}
}
private void displayInvalidInputMessage()
{
Alert errorAlert = new Alert(Alert.AlertType.ERROR);
errorAlert.setHeaderText("Invalid input");
errorAlert.setContentText("Text, category and date must all be specified.");
errorAlert.showAndWait();
}
private void createAndAddToDoItem(String text, ToDoCategory category, LocalDate date)
{
ToDoItem item = new ToDoItem(text, category, date);
this.todoList.getItems().add(item);
}
@FXML
protected void onRemoveButtonClick()
{
int indexToRemove = this.todoList.getSelectionModel().getSelectedIndex();
this.todoList.getItems().remove(indexToRemove);
}
private MutableList<ToDoItem> readToDoListFromFile()
{
ObjectMapper mapper = this.getObjectMapper();
MutableList<ToDoItem> list = null;
try
{
list = mapper.readValue(
Paths.get("todolist.json").toFile(),
new TypeReference<MutableList<ToDoItem>>() {});
return list;
}
catch (IOException e)
{
System.out.println(e);
}
return Lists.mutable.empty();
}
public void writeToDoListToFile()
{
ObjectMapper mapper = this.getObjectMapper();
MutableList<ToDoItem> list =
this.todoList.getItems().stream()
.collect(Collectors2.toList());
try
{
mapper.writeValue(
Paths.get("todolist.json").toFile(),
list);
}
catch (IOException e)
{
throw new RuntimeException(e);
}
}
private ObjectMapper getObjectMapper()
{
ObjectMapper mapper = new ObjectMapper()
.registerModule(new EclipseCollectionsModule())
.registerModule(new JavaTimeModule());
return mapper;
}
public record ToDoItem(
@JsonProperty String name,
@JsonProperty ToDoCategory category,
@JsonProperty LocalDate date)
{
@JsonIgnore
public String getCategory()
{
return this.category.getEmoji();
}
@JsonIgnore
public String getName()
{
return this.name;
}
@JsonIgnore
public String getDate()
{
return this.date.toString();
}
}
public enum ToDoCategory
{
EXERCISE("🚴"),
WORK("📊"),
RELAX("🧘"),
TV("📺"),
READ("📚"),
EVENT("🎭"),
CODE("💻"),
COFFEE("☕️"),
EAT("🍽"),
SHOP("🛒"),
SLEEP("😴");
private String emoji;
ToDoCategory(String emoji)
{
this.emoji = emoji;
}
public String getEmoji()
{
return this.emoji;
}
}
}
module-info.java
module example.todolist {
requires javafx.controls;
requires javafx.fxml;
requires org.eclipse.collections.api;
requires org.eclipse.collections.impl;
requires com.fasterxml.jackson.annotation;
requires com.fasterxml.jackson.core;
requires com.fasterxml.jackson.databind;
requires com.fasterxml.jackson.datatype.jsr310;
requires jackson.datatype.eclipse.collections;
opens example.todolist;
}
The Future of my TodoListApplication
Every time I finish something with my TodoListApplication
, there seems like there is more I want to do. I guess that makes sense. We always have to dos. Here are some things I might experiment with in my TodoListApplication
in the future.
Final Thoughts
I hope you enjoyed this first set of blogs for my exploration of JavaFX and Jackson. I learned a lot in the process, and have finally opened my eyes wide to the wonderful world of JavaFX UI development. There seems to be a very vibrant community around JavaFX and some of the stuff I have seen recently really looks amazing. I am looking forward to continue my learning in this space.
Thank you for reading!
I am the creator of and a Committer for the Eclipse Collections OSS project which is managed at the Eclipse Foundation. Eclipse Collections is open for contributions.
Other JSON tutorials in Java you may like
- How to format JSON String in Java?
- 3 Ways to Convert JSON String to Object in Java
- How to parse JSON using Gson?
- How to parse a JSON array in Java?
- How to Iterate over JSONObject in json-simple Java?
- How to download Jackson JAR files in Java
- How to Solve UnrecognizedPropertyException: Unrecognized field, not marked as ignorable -
- 20 JSON Interview Questions with Answers
- How to convert JSON to HashMap in Java?
- 10 Things Java developers should learn
- How to ignore unknown properties while parsing JSON in Java?
- How to return JSON from Spring MVC controller?