Thursday, November 29, 2012

GSON Exclude / Skip class fields

I've been working on a project recently that uses GSON library to serialize POJO's  in JSON and store them in single database field. (We used this strategy for flexibility and ease of maintenance - no need for updating database schema every time model changes, but that's outside of this post topic). Since all objects where identified by their unique id property (integer or UUID), which was their primary key in RMDBS, there was no need to store this field in object's JSON representation. First idea that came is just to mark this field as transient

 public class Person {   
    ... field declarations ...  
    private transient int id;   
     ... getters / setters ..  
 }  

However, I had two problem's with this, one of pure technical nature, and one semantic

  • Keyword transient should be use to mark fields that are not supposed to persist. However, here's this is not the case, since id member IS stored in relational database.
  • This object are further exposed via SOAP service, thus id information would be lost when serializing object to XML
Digging further int GSON library, I've found that GsonBuilder has method setExclusionStrategies, which is pretty clear what id does, so when creating serializer we can specify exclusion strategy

  com.google.gson.Gson serializer = new com.google.gson.GsonBuilder().
     setExclusionStrategies(new com.google.gson.ExclusionStrategy() {  
       public boolean shouldSkipField(com.google.gson.FieldAttributes fieldAttributes) {  
         return "Person".equals((fieldAttributes.getDeclaredClass().getName())) && "id".equals(fieldAttributes.getName());  
       }  
       public boolean shouldSkipClass(Class<?> arg0) {  
         return false;  
       }  
     }).create();  


As you can see, all you have to do is implement com.google.gson.ExclusionStrategy interface. However, as mad as I am for generic and maintainable solutions, I did not like the fact this code is not extensible in easy and readable way, in term if there was a class User with property password I don't want to serialize, shouldSkipField method would look like:


  public boolean shouldSkipField(com.google.gson.FieldAttributes fieldAttributes) {  
         return "Person".equals((fieldAttributes.getDeclaredClass().getName())) && "id".equals(fieldAttributes.getName()) ||  
             "User".equals((fieldAttributes.getDeclaredClass().getName())) && "password".equals(fieldAttributes.getName());  
       }  

Quite ugly, right, specially, if there is more classes and fields to skip than two, we would get heavy-readable boolean expression. This can be improved, by simple externalizing 'configuration' outside of shouldSkipField and shouldSkipClass methods logic.  It's doable by simple storing this configuration as HashMap:

 package com.myapp.serialization  
   
 import java.util.Arrays;  
 import java.util.HashMap;  
 import java.util.Map.Entry;  
   
 import com.google.gson.ExclusionStrategy;  
 import com.google.gson.FieldAttributes;  
   
 public class JSONExclusionStrategy implements ExclusionStrategy{  
   
         private static HashMap<Class<?>,String[]> excludedFields;  
           
         public JSONExclusionStrategy(){  
             excludedFields = new HashMap<>();  
             //class maps to array of fields to skip in class  
             excludedFields.put(com.myapp.model.Person, new String[]{"id"});  
             excludedFields.put(com.myapp.model.User, new String[]{"password"});  
               
             //all arrays of fields are sorted lexically for faster lookup  
             for(Entry<Class<?>,String[]> entry : excludedFields.entrySet()){  
                 Arrays.sort(entry.getValue());  
             }  
         }  
           
   
         public boolean shouldSkipClass(Class<?> arg0) {  
             return false;  
         }  
   
         public boolean shouldSkipField(FieldAttributes fieldAttributes) {  
             if(excludedFields.containsKey(fieldAttributes.getDeclaredClass())){  
                 return Arrays.binarySearch(excludedFields.get(fieldAttributes.getDeclaredClass()),fieldAttributes.getName())>=0;   
             }  
             return false;  
         }  
 }  
   

 ...  
   com.google.gson.Gson serializer = new com.google.gson.GsonBuilder()  
     .setExclusionStrategies( new com.myapp.serialization.JSONExclusionStrategy()).create();  
 ..  


If further separation of configuration and code, we could wrap excluededFields dictionary into seprate class, and populate it from properties file or XML file. Another idea is to create annotation which would mark field as not serializable to JSON (and than read field's annotation in shouldSkipField method), though this requires code change. Hope anyone finds this post useful.

3 comments:

  1. "We used this strategy for flexibility and ease of maintenance - no need for updating database schema every time model changes, but that's outside of this post topic."

    We want more! We want more! :)

    ReplyDelete
  2. Nice technique. Thanks for sharing.

    ReplyDelete
    Replies
    1. Thanks! Though this outlines technique that's not really optimized, it demonstrates concept. Currently approach above takes O(log N) time, while it could be O(1) by using simple if statements rather than using binary search in array when checking if field should be skipped.

      Delete