Javarevisited
Published in

Javarevisited

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

--

--

Get the Medium app

A button that says 'Download on the App Store', and if clicked it will lead you to the iOS App store
A button that says 'Get it on, Google Play', and if clicked it will lead you to the Google Play store
Donald Raab

Java Champion. Creator of the Eclipse Collections OSS Java library (http://www.eclipse.org/collections/). Inspired by Smalltalk. Opinions are my own.