Jackson/Gson serialize and deserialize JavaFX Properties to json

两盒软妹~` 提交于 2021-01-28 05:30:29

问题


I have added a BooleanProperty into a DAO class which is to be serialized into JSON and sent across to a server to be saved in a MySQL database. The reason I am using the BooleanProperty is because I want to use data binding for the 'isActive' field in my JavaFX desktop application.

The class to be serialized:

package com.example.myapplication

import lombok.Data;
import javafx.beans.property.BooleanProperty;
import javafx.beans.property.SimpleBooleanProperty;

@Data
public class MyDAO {
    private int id;
    private String firstName;
    private String lastname;
    private final BooleanProperty isActive = new SimpleBooleanProperty();
}

I am serializing to JSON using Gson:

StringEntity entity = new StringEntity(new Gson().toJson(myDTO), "UTF-8");

When this gets serialized to JSON, it looks like this:

{
   "id":0,
   "first_name":"Joe",
   "last_name":"Bloggs",
   "is_active":{
      "name":"",
      "value":false,
      "valid":true

   }
}

This is causing problems on the server when deserializing (using Jackson), as the server expects a boolean value to correspond with what will be saved in the database. Is there a way to just get the true/false value deserialized from the BooleanProperty?

This is what I would like to see in the server:

{
   "id":0,
   "first_name":"Joe",
   "last_name":"Bloggs",
   "is_active": false,
}

回答1:


Your client app uses Gson to serialise POJO to JSON and server app uses Jackson to deserialise JSON back to POJO. In both cases these two libraries by default serialise provided classes as regular POJO-s. In your POJO you use JavaFX Properties which are wrappers for values with extra functionality. When you serialise POJO to JSON sometimes you need to hide internal implementation of POJO and this is why you should implement custom serialiser or use FX Gson library.

1. Custom serialiser

To write custom serialiser you need to implement com.google.gson.JsonSerializer interface. Below you can find an example hot it could look like:

import com.google.gson.Gson;
import com.google.gson.GsonBuilder;
import com.google.gson.JsonElement;
import com.google.gson.JsonPrimitive;
import com.google.gson.JsonSerializationContext;
import com.google.gson.JsonSerializer;
import javafx.beans.property.BooleanProperty;
import javafx.beans.property.SimpleBooleanProperty;
import lombok.Data;

import java.lang.reflect.Type;

public class GsonApp {

    public static void main(String[] args) {
        MyDAO myDAO = new MyDAO();
        myDAO.setId(1);
        myDAO.setFirstName("Vika");
        myDAO.setLastname("Zet");
        myDAO.getIsActive().set(true);

        Gson gson = new GsonBuilder()
                .registerTypeAdapter(BooleanProperty.class, new BooleanPropertySerializer())
                .setPrettyPrinting().create();
        System.out.println(gson.toJson(myDAO));
    }

}

class BooleanPropertySerializer implements JsonSerializer<BooleanProperty> {
    @Override
    public JsonElement serialize(BooleanProperty src, Type typeOfSrc, JsonSerializationContext context) {
        return new JsonPrimitive(src.getValue());
    }
}

@Data
class MyDAO {
    private int id;
    private String firstName;
    private String lastname;
    private final BooleanProperty isActive = new SimpleBooleanProperty();
}

Above code prints:

{
  "id": 1,
  "firstName": "Vika",
  "lastname": "Zet",
  "isActive": true
}

2. FX Gson

In case you use many types from javafx.beans.property.* package good idea would be to use FX Gson library which implement custom serialisers for most used types. You just need to add one extra dependency to your Maven POM file:

<dependency>
    <groupId>org.hildan.fxgson</groupId>
    <artifactId>fx-gson</artifactId>
    <version>3.1.2</version>
</dependency>

Example usage:

import com.google.gson.Gson;
import javafx.beans.property.BooleanProperty;
import javafx.beans.property.SimpleBooleanProperty;
import lombok.Data;
import org.hildan.fxgson.FxGson;

public class GsonApp {

    public static void main(String[] args) {
        MyDAO myDAO = new MyDAO();
        myDAO.setId(1);
        myDAO.setFirstName("Vika");
        myDAO.setLastname("Zet");
        myDAO.getIsActive().set(true);

        Gson gson = FxGson.coreBuilder().setPrettyPrinting().create();
        System.out.println(gson.toJson(myDAO));
    }

}

Above code prints:

{
  "id": 1,
  "firstName": "Vika",
  "lastname": "Zet",
  "isActive": true
}



回答2:


It's not clear if you're using Jackson, or Gson for your JSON serialization framework, and these will behave differently in this example.

The bottom line here is that if you use any framework in conjunction with JavaFX properties, it needs to fully support and respect encapsulation. Both Lombok (which makes assumptions about the relationship between your fields and property methods), and Gson (which bypasses property methods entirely) do not support encapsulation to the extent needed.

The property naming pattern that JavaFX properties expect is this:

public class MyDAO {
    // ...
    private final BooleanProperty active = new SimpleBooleanProperty();

    public BooleanProperty activeProperty() {
        return active ;
    }

    public final boolean isActive() {
        return activeProperty().get();
    }

    public final void setActive(boolean active) {
        activeProperty().set(active);
    }

}

From a Java Bean properties perspective (i.e. a perspective that properly supports encapsulation), this class has a boolean readable-writable property called active. The fact that is implemented using a JavaFX property is essentially an implementation detail of the class (albeit one which provides additional functionality via the activeProperty() method).

Lombok simply assumes you want get and/or set methods for your fields that define properties of the same type (and name), which simply doesn't work in this case. So my advice would be to not use Lombok for this class (actually, my advice would be never to use Lombok for these very reasons, but that's up to you):

public class MyDAO {
    private int id;
    private String firstName;
    private String lastName;
    private final BooleanProperty isActive = new SimpleBooleanProperty();
    public int getId() {
        return id;
    }
    public void setId(int id) {
        this.id = id;
    }
    public String getFirstName() {
        return firstName;
    }
    public void setFirstName(String firstName) {
        this.firstName = firstName;
    }
    public String getLastName() {
        return lastName;
    }
    public void setLastName(String lastName) {
        this.lastName = lastName;
    }

    public BooleanProperty activeProperty() {
        return isActive ;
    }

    public final boolean isActive() {
        return activeProperty().get();
    }

    public final void setActive(boolean active) {
        activeProperty().set(active);
    }
    @Override
    public int hashCode() {
        final int prime = 31;
        int result = 1;
        result = prime * result + ((firstName == null) ? 0 : firstName.hashCode());
        result = prime * result + id;
        result = prime * result + ((isActive()) ? 0 : 1);
        result = prime * result + ((lastName == null) ? 0 : lastName.hashCode());
        return result;
    }
    @Override
    public boolean equals(Object obj) {
        if (this == obj)
            return true;
        if (obj == null)
            return false;
        if (getClass() != obj.getClass())
            return false;
        MyDAO other = (MyDAO) obj;
        if (firstName == null) {
            if (other.firstName != null)
                return false;
        } else if (!firstName.equals(other.firstName))
            return false;
        if (id != other.id)
            return false;
        if (isActive() != other.isActive()) {
                return false;
        }
        if (lastName == null) {
            if (other.lastName != null)
                return false;
        } else if (!lastName.equals(other.lastName))
            return false;
        return true;
    }
    @Override
    public String toString() {
        return "MyDAO [getId()=" + getId() + ", getFirstName()=" + getFirstName() + ", getLastName()="
                + getLastName() + ", isActive()=" + isActive() + "]";
    }


}

(While that looks like a lot of code, the only part I had to type was the activeProperty(), isActive(), and setActive() methods; the rest was generated in about 10 mouse clicks in Eclipse. E(fx)clipse provides point-and-click functionality for the methods I typed, I just don't have it installed in the version of Eclipse I'm using.)

If you're really wedded to Lombok, I think you can do something like (but, not being a Lombok user, I am not certain if this will work):

@Data
public class MyDAO {
    private int id;
    private String firstName;
    private String lastName;
    @Getter(AccessLevel.NONE)
    @Setter(AccessLevel.NONE)
    private final BooleanProperty isActive = new SimpleBooleanProperty();

    public BooleanProperty activeProperty() {
        return isActive ;
    }

    public final boolean isActive() {
        return activeProperty().get();
    }

    public final void setActive(boolean active) {
        activeProperty().set(active);
    }
}

Similarly, GSON doesn't respect encapsulation, and tries to replicate your fields instead of your properties (and it appears there's no equivalent to JPA's "field access" versus "property access" functionality, nor any desire to provide it). I favor Jackson for this reason: using Jackson the serialized version is generated via properties, and would look the way you want it directly out of the box:

MyDAO bean = new MyDAO();
bean.setFirstName("Joe");
bean.setLastName("Bloggs");
bean.setActive(false);

StringWriter w = new StringWriter();
ObjectMapper jackson = new ObjectMapper();
jackson.writeValue(w, bean);
System.out.println(w.toString()) ;
// output:  {"firstName": "Joe", "lastName":"Bloggs", "active":false}

With GSON, you'll need a type adapter to handle anything using JavaFX properties. (There's probably a way to write a factory that generates type adapters for the properties themselves, but given the number of different possible types (including user-defined implementations of the property interfaces), that's probably pretty difficult to do.)

public class MyDAOTypeAdapter extends TypeAdapter<MyDAO> {

    @Override
    public void write(JsonWriter out, MyDAO value) throws IOException {
        out.beginObject();
        out.name("id").value(value.getId());
        out.name("firstName").value(value.getFirstName());
        out.name("lastName").value(value.getLastName());
        out.name("active").value(value.isActive());
        out.endObject();
    }

    @Override
    public MyDAO read(JsonReader in) throws IOException {
        MyDAO bean = new MyDAO();
        in.beginObject();
        while (in.hasNext()) {
            // should really handle nulls for the strings...
            switch(in.nextName()) {
            case "id":
                bean.setId(in.nextInt());
                break ;
            case "firstName":
                bean.setFirstName(in.nextString());
                break ;
            case "lastName":
                bean.setLastName(in.nextString());
                break ;
            case "active":
                bean.setActive(in.nextBoolean());
                break ;
            }
        }
        in.endObject();
        return bean ;
    }

}

Here's a test for this:

public class Test {

    public static void main(String[] args) throws JsonGenerationException, JsonMappingException, IOException {
        MyDAO bean = new MyDAO();
        ObjectMapper mapper = new ObjectMapper();
        bean.setFirstName("Joe");
        bean.setLastName("Boggs");
        bean.setActive(true);
        StringWriter w = new StringWriter();
        mapper.writeValue(w, bean);
        String output = w.toString() ;
        System.out.println("Jackson Serialized version:\n"+output);

        MyDAO jacksonBean = mapper.readValue(output, MyDAO.class);
        System.out.println("\nJackson Deserialized bean:\n" + jacksonBean);

        GsonBuilder gsonBuilder = new GsonBuilder();        
        gsonBuilder.registerTypeAdapter(MyDAO.class, new MyDAOTypeAdapter());
        gsonBuilder.setPrettyPrinting();
        Gson gson = gsonBuilder.create();

        w.getBuffer().delete(0, w.getBuffer().length());
        gson.toJson(bean, w);
        String gsonOutput = w.toString() ;
        System.out.println("\nGSON serialized version:\n"+gsonOutput);

        MyDAO gsonBean = gson.fromJson(gsonOutput, MyDAO.class);
        System.out.println("\nGSON deserialized bean:\n"+gsonBean);

    }
}

which generates the following output:

Jackson Serialized version:
{"id":0,"firstName":"Joe","lastName":"Boggs","active":true}

Jackson Deserialized bean:
MyDAO [getId()=0, getFirstName()=Joe, getLastName()=Boggs, isActive()=true]

GSON serialized version:
{
  "id": 0,
  "firstName": "Joe",
  "lastName": "Boggs",
  "active": true
}

GSON deserialized bean:
MyDAO [getId()=0, getFirstName()=Joe, getLastName()=Boggs, isActive()=true]



回答3:


I believe the answer is to mark the field you don't want to be serialized as transient and add the boolean field containing the value.

@Data
public class MyDAO {
    private int id;
    private String firstName;
    private String lastname;
    private transient final BooleanProperty booleanProp = new SimpleBooleanProperty();
    private boolean isActive = booleanProp.get();
}

The main issue is that you are delegating the responsibility of serializing your objects to a 3rd party. You could also serialize it yourself to control the serialization behavior. It will get tricky if you want to do something like serialize the boolean value when it is false but the actual BooleanProperty object when the value is true. That is, unless BooleanProperty does that by default, which it does not.



来源:https://stackoverflow.com/questions/60455801/jackson-gson-serialize-and-deserialize-javafx-properties-to-json

易学教程内所有资源均来自网络或用户发布的内容,如有违反法律规定的内容欢迎反馈
该文章没有解决你所遇到的问题?点击提问,说说你的问题,让更多的人一起探讨吧!