16

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.

Joshua F
  • 878
  • 8
  • 11

3 Answers3

22

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
  }
}
Joffrey
  • 32,348
  • 6
  • 68
  • 100
  • The FxGson documentation does not mention it, but the Core Builder also contains support for `ObjectProperty` types. – Joe Almore Feb 06 '17 at 15:33
  • @JoeAlmore Actually it does, in [this detailed wiki page](https://github.com/joffrey-bion/fx-gson/wiki/Built-in-GsonBuilders) about the builders, but not in the readme, indeed. – Joffrey Feb 06 '17 at 19:15
  • Yes, I could see it later as `Property`. – Joe Almore Feb 06 '17 at 19:19
  • This is good library. Thanks for sharing. Is it possible for you to put this in maven central? It will be really helpful. – Pavan Mar 23 '17 at 09:27
  • @Pavan I'm working on it, yes, because I know it's not very comfortable for maven users to use JCenter. You may upvote/follow [the issue here](https://github.com/joffrey-bion/fx-gson/issues/12). – Joffrey Mar 23 '17 at 10:41
  • 1
    @Pavan as of version 3.1.0, it is now available on Maven Central – Joffrey Mar 23 '17 at 23:48
  • This is great. Appreciate a lot Joffrey – Pavan Apr 10 '17 at 09:51
4

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

Community
  • 1
  • 1
Waxren
  • 2,002
  • 4
  • 30
  • 43
3

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));
ATutorMe
  • 820
  • 8
  • 14
utrescu
  • 374
  • 2
  • 6
  • I'm really thankful for your answer, it solved the problem for me. If me can not read your answer It may take a lot of effort to migrate the JavaFX Property field to basic Java – Touya Akira Jul 17 '18 at 13:53