Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Serialize JavaFX model with GSON

Tags:

java

gson

javafx

I'm currently following a tutorial to help me learn how JavaFX works and in the tutorial they're building a small app to manage peoples information. The tutorial is also using XML for loading/saving but I do not want to use XML and would like to use JSON. I have a Person model that uses StringProperty, IntegerProperty and ObjectProperty. My issue is that I'm not exactly sure what the best way to load and save this would be without it saving unnecessary fields and also loading without Gson throwing an error.

Person

import java.time.LocalDate;

import javafx.beans.property.IntegerProperty;
import javafx.beans.property.ObjectProperty;
import javafx.beans.property.SimpleIntegerProperty;
import javafx.beans.property.SimpleObjectProperty;
import javafx.beans.property.SimpleStringProperty;
import javafx.beans.property.StringProperty;

/**
 * Model class for a Person.
 *
 * @author Marco Jakob
 */
public class Person {

    private final StringProperty firstName;

    private final StringProperty lastName;

    private final StringProperty street;

    private final IntegerProperty postalCode;

    private final StringProperty city;

    private final ObjectProperty<LocalDate> birthday;

    /**
     * Default constructor.
     */
    public Person() {
        this(null, null);
    }

    /**
     * Constructor with some initial data.
     * 
     * @param firstName
     * @param lastName
     */
    public Person(String firstName, String lastName) {
        this.firstName = new SimpleStringProperty(firstName);
        this.lastName = new SimpleStringProperty(lastName);

        // Some initial dummy data, just for convenient testing.
        this.street = new SimpleStringProperty("some street");
        this.postalCode = new SimpleIntegerProperty(1234);
        this.city = new SimpleStringProperty("some city");
        this.birthday = new SimpleObjectProperty<LocalDate>(LocalDate.of(1999, 2, 21));
    }

    public String getFirstName() {
        return firstName.get();
    }

    public void setFirstName(String firstName) {
        this.firstName.set(firstName);
    }

    public StringProperty firstNameProperty() {
        return firstName;
    }

    public String getLastName() {
        return lastName.get();
    }

    public void setLastName(String lastName) {
        this.lastName.set(lastName);
    }

    public StringProperty lastNameProperty() {
        return lastName;
    }

    public String getStreet() {
        return street.get();
    }

    public void setStreet(String street) {
        this.street.set(street);
    }

    public StringProperty streetProperty() {
        return street;
    }

    public int getPostalCode() {
        return postalCode.get();
    }

    public void setPostalCode(int postalCode) {
        this.postalCode.set(postalCode);
    }

    public IntegerProperty postalCodeProperty() {
        return postalCode;
    }

    public String getCity() {
        return city.get();
    }

    public void setCity(String city) {
        this.city.set(city);
    }

    public StringProperty cityProperty() {
        return city;
    }

    public LocalDate getBirthday() {
        return birthday.get();
    }

    public void setBirthday(LocalDate birthday) {
        this.birthday.set(birthday);
    }

    public ObjectProperty<LocalDate> birthdayProperty() {
        return birthday;
    }
}

Saving where personData is an ObservableList of Persons

try (Writer writer = new FileWriter(file)) {
    new GsonBuilder().setPrettyPrinting().disableHtmlEscaping().create().toJson(personData, writer);
}

This way of saving currently produces a save with a lot of unnecessary fields like name, value, etc when it could be "firstName": "Hans"

[{
    "firstName": {
        "name": "",
        "value": "Hans",
        "valid": true,
        "helper": {
            "observable": {}
        }
    },
    "lastName": {
        "name": "",
        "value": "Muster",
        "valid": true,
        "helper": {
            "observable": {}
        }
    },
    "street": {
        "name": "",
        "value": "some street",
        "valid": true
    },
    "postalCode": {
        "name": "",
        "value": 1234,
        "valid": true
    },
    "city": {
        "name": "",
        "value": "some city",
        "valid": true
    },
    "birthday": {}
}]

Now when even trying to load the string above with Gson it produces an error, Failed to invoke public javafx.beans.property.StringProperty() with no args.

Loader

Person[] persons;

try (Reader reader = new FileReader(file)) {
    persons = gson.fromJson(reader, Person[].class);
}

personData.clear();
personData.addAll(persons);

I've Googled to see if it was possible to use getters and setters with Gson but it doesn't really seem possible so I'm stuck on what to do.

like image 557
Joshua F Avatar asked Sep 26 '15 06:09

Joshua F


3 Answers

I have faced the same issue with GSON and JavaFX Property Model.And I have resolved it using LinkedHashMap like the following :-

in your model class :-

public Person(LinkedHashMap<String, Object> personData) {
    this.firstName = new SimpleStringProperty((String) personData.get("firstName"));
    this.lastName = new SimpleStringProperty((String) personData.get("lastName"));

    this.street = new SimpleStringProperty((String) personData.get("street"));
    this.postalCode = new SimpleIntegerProperty(((Double) personData.get("postalCode")).intValue());
    this.city = new SimpleStringProperty((String) personData.get("city"));

    String birthdayString = (String) personData.get("birthday");
    LocalDate date = LocalDate.parse(birthdayString ,DateTimeFormatter.ofPattern("yyy, mm, dd"));
    this.birthday = new SimpleObjectProperty<LocalDate>(date);
}

public LinkedHashMap<String, Object> getPersonData() {
    LinkedHashMap<String, Object> personData = new LinkedHashMap<>();

    personData.put("firstName", firstName.getValue());
    personData.put("lastName", lastName.getValue());
    personData.put("street", street.getValue());
    personData.put("postalCode", postalCode.getValue());
    personData.put("city", city.getValue());
    personData.put("birthday", birthday.getValue());

    return personData;
}

Then in loader :-

Gson gson = new Gson();
List<LinkedHashMap<String, Object>> persons = new Gson().fromJson(jsonData, new TypeToken<List<LinkedHashMap<String, Object>>>() {}.getType());

for(LinkedHashMap<String, Object> personData : persons) {
    Person person = new Person(personData);
}

And to Convert to Json :-

LinkedHashMap<String, Object> personData = person.getPersonData();

String jsonData = new Gson().toJson(personData);

Notice that GSON maps int value to double as it is more generic so you need to cast the postal code to double first and then get the int value from it refer to this question for more info.

How to prevent Gson from expressing integers as floats

like image 35
Waxren Avatar answered Oct 11 '22 02:10

Waxren


I know I'm a bit late to the party, but this is for future readers.

I had the exact same problem. I ended up writing a bunch of Gson TypeAdapters, one for each JavaFX property type (and a couple more for Color and Font).

I gathered them all in a lightweight library called FxGson (< 30kB).

Now, simply by using FxGson's GsonBuilder, the JavaFX POJOs will be serialized as if their properties were simple values. Using the Person class in your example:

Person p = new Person("Hans", "Muster");
Gson gson = FxGson.coreBuilder().setPrettyPrinting().disableHtmlEscaping().create();
System.out.println(gson.toJson(p));

This outputs:

{
  "firstName": "Hans",
  "lastName": "Muster",
  "street": "some street",
  "postalCode": 1234,
  "city": "some city",
  "birthday": {
    "year": 1999,
    "month": 2,
    "day": 21
  }
}
like image 123
Joffrey Avatar answered Oct 11 '22 04:10

Joffrey


Are GSON a requirement?

I have the same problem with GSON and switched to Jackson. It works:

ObjectMapper mapper = new ObjectMapper();
System.out.println(mapper.writeValueAsString(persons));
like image 39
utrescu Avatar answered Oct 11 '22 02:10

utrescu