Monday, July 18, 2016

[DevNote] Custom Jackson Serializer/Deserializer


Custom Jackson Serializer/Deserializer


When building your RESTful APIs, we will be dealing with how to render our objects as JSON format. Doing flattening and restructuring of objects to JSON string can be tricky and boring, luckily as we have Jackson to our rescue. This article will present a full overview to how to use Jackson to convert our object to JSON string and reconstruct them from JSON string. 

Keywords: Jackson, JSON, Java, Spring, Morphia, MongoDB, RESTFul

1. Standard serializer


Jackson provides default serializer for any POJO objects, to use this simply use the ObjectMapper object to serialize objects into JSON string.

For example:

Data Model:
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
public class DriverModel {
 public String netName;
 private String fromPin;
 private String toPin;
 private float groundCapacitance;
 private List<DriverModel> aggressors;
 
 //------<getter/setter> for private fields
 public String getFromPinName() {
  return this.fromPin;
 }
 
 public void setFromPinName(String fromPin) {
  this.fromPin = fromPin;
 }
}

Client Code (serializing):
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
  try {
   ObjectMapper mapper = new ObjectMapper();
   DriverModel driverModel = new DriverModel("n1", "U01/A", "U01/Y", (float) 0.5);

   List<DriverModel> aggressors = new ArrayList<DriverModel>();
   aggressors.add(new DriverModel("a1", "U02/A", "U02/Y", (float)0.88));
   aggressors.add(new DriverModel("a2", "U03/A", "U03/Y", (float)0.66));
   driverModel.setAggressors(aggressors);
   
   String jsonString = mapper.writeValueAsString(driverModel);
   System.out.println(jsonString);
  } catch (JsonProcessingException e) {
   // TODO Auto-generated catch block
   e.printStackTrace();
  }

JSON string:
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
{
 "netName": "n1",
 "aggressors": [{
  "netName": "a1",
  "aggressors": null,
  "fromPinName": "U02/A",
  "toPinName": "U02/Y",
  "groupCapacitance": 0.88
 }, {
  "netName": "a2",
  "aggressors": null,
  "fromPinName": "U03/A",
  "toPinName": "U03/Y",
  "groupCapacitance": 0.66
 }],
 "fromPinName": "U01/A",
 "toPinName": "U01/Y",
 "groupCapacitance": 0.5
}

Public fields will be guaranteed to be serialized and deserialized by the default serializer, private fields requires getters and setters to allow Jackson to access them. And the key string of the JSON object will be whatever is after get/set. So the required pattern for setters/getters are get<key>, set<key>.

Straightforward enough right?

2. Usage in Spring MVC and Loop Breaking


When you use Spring to write your RESTfull API if you use ResponseEntity<T> as return value from the controller to package your models, the default ObjectMapper will be used on the specified object type. So usually we only focus on the data modeling of model objects. But using the default serializer & deserializer can be messy sometimes, suppose we have the following two models.

Data Model:
1
2
3
4
5
6
7
8
9
public class User {
 private Address address;
 private String name;
}

public class Address {
 private String address;
 private User user;
}

We have a one-to-one relationship from User to Address and the same thing from Address to User. When we use the default serializer, what ended happening is a stack overflow because the serializer will get stuck back and forth between the User and Address object.

There are typical two solutions, one is to use @JsonManagedReference and @JsonBackReference, the other is to use @JsonIdentityInfo

Example using @JsonManagedReference and @JsonBackReference:
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
public class User { 
 @JsonManagedReference
 private Address address;
 private String name;
}
public class Address {
 private String address;
        @JsonBackReference
 private User user;
}

Example using @JsonIdentityInfo:
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
@JsonIdentityInfo(generator=ObjectIdGenerators.IntSequenceGenerator.class, property="@id")
public class User {
 
 private Address address;
 private String name;
}

@JsonIdentityInfo(generator=ObjectIdGenerators.IntSequenceGenerator.class, property="@id")
public class Address {
 
 private String address;
 private User user;
}

Resulting JSON string:
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
{
 "@id": 1,
 "address": {
  "@id": 2,
  "address": "This is my address",
  "user": 1
 },
 "name": "Home Owner"
}

{
 "@id": 1,
 "address": "This is my address",
 "user": {
  "@id": 2,
  "address": 1,
  "name": "Home Owner"
 }
}

@JsonIdentityInfo will add a special field (we can use "property" field to specify the field name) pretty much like visited flag when traversing the object relation map so loop can be detected and stopped. 

To break the loop, we actually just need to add the @JsonIdentityInfo to one of the model, in my example I added to both of them.

3. Full Custom Serializer/Deserializer


Using Jackson annotations can solve the endless loop when serializing complex objects, however in most cases this is not enough. So we need to make our own tailor-made serializer and deserializer.

The third option to address the above looping problem is to fully customize the serializer and deserializer. Jackson provides JsonSerializer<T> and JsonDeserializer<T> abstract classes, to make our own serializer/deserializer when just need to extend those abstract classes like the following:

Data model:
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
@Entity(value = AppEntry.collectionName)
@JsonSerialize(using=AppEntrySerializer.class)
@JsonDeserialize(using=AppEntryDeserializer.class)
public class AppEntry {
 
 public static final String collectionName = "applications";
 
 @Id
 private ObjectId id; //mongodb object_id field (_id)
 
 private String title;
 private String imageLink;
 private String sourceLink;
 private String description;
 private String category;
 private float price;
}

Serializer:
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
public class AppEntrySerializer extends JsonSerializer<AppEntry> {

 @Override
 public void serialize(AppEntry value, JsonGenerator gen, SerializerProvider serializers)
   throws IOException, JsonProcessingException {
  gen.writeStartObject();
  gen.writeStringField("id", value.getId().toString());
  gen.writeStringField("title", value.getTitle());
  gen.writeStringField("imageLink", value.getImageLink());
  gen.writeStringField("sourceLink", value.getSourceLink());
  gen.writeStringField("description", value.getDescription());
  gen.writeStringField("category", value.getCategory());
  gen.writeNumberField("price", value.getPrice());
       }
}

Deserializer:
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
public class AppEntryDeserializer extends JsonDeserializer<AppEntry> {

 @Override
 public AppEntry deserialize(JsonParser p, DeserializationContext ctxt) throws IOException, JsonProcessingException {
  AppEntry app = new AppEntry();
  ObjectCodec object = p.getCodec();
  JsonNode node = object.readTree(p);
  
  app.setId(node.get("id").textValue());
  
  app.setCategory(node.get("category").textValue());
  app.setDescription(node.get("description").textValue());
  app.setImage(node.get("imageLink").textValue());
  app.setTitle(node.get("title").textValue());
  app.setSourceLink(node.get("sourceLink").textValue());
  app.setPrice(node.get("price").floatValue());
  
  return app;
 }

}

To configure the serializer and deserializer Jackson provides @JsonSerialize and @JsonDeserialize annotations, which is a shorthand configuration compared to the following:

1
2
3
4
  ObjectMapper mapper = new ObjectMapper();
  SimpleModule module = new SimpleModule();
  module.addSerializer(AppEntry.class, new AppEntrySerializer());
  mapper.registerModule(module);


Conclusion:


1. We can use default serializer and deserializer to convert simple objects to/from JSON
2. @JsonManagedReference/@JsonBackReference and @JsonIdentityInfo provides some customization to solve dead loop problem
3. We can extend JsonSerializer<T>/JsonDeserializer<T> abstract classes to build fully customized serializer/deserializer and use @JsonSerialize/@JsonDeserialize.


No comments:

Post a Comment