Gson serialize null values only when the field is in a nested object

Viewed 1485

The problem I am facing is that I want to ser/des null values when only it comes to non top-level attributes, and I have no idea how to achieve that. So let's say I have a User class:

Class User {
   String name;
   int id;
   Address address;
}

And an Address class:

Class Address{
   String street;
   String city;
   String country;
}

Right now, I can use below Gson instance to ser/des null values:

Gson gson = new GsonBuilder().serializeNulls().create();
Address address = new Address(null, "New York", "US");
User user = new User("Adam", 123, address);  
String userJson = gson.toJson(user); 

Output is:

{
  "name": "Adam",
  "id": 123,
  "address": {
      "street": null,
      "city": "New York",
      "country": "US"
  }
}

However, I do NOT want to ser/des nulls when it comes to top-level attributes of User. For example for below User:

User user = new User("Adam", 123, null);

I want to have an output as below and without address field:

{
  "name": "Adam",
  "id": 123
}

I am now trying to use a customized serializer to hardcode every top-level attributes and remove them if they are null:

public class SerializerForUser implements JsonSerializer<ConfigSnapshot> {

    @Override
    public JsonElement serialize(User user, Type type, JsonSerializationContext jsc) {
        Gson gson = new GsonBuilder().serializeNulls().create();
        JsonObject jsonObject = gson.toJsonTree(user).getAsJsonObject();
        if (user.getAddress() == null) {
            jsonObject.remove("address");
        }
        // if... (same with other top-level attributes)
        return jsonObject;
    }
}

Gson gson = new GsonBuilder().serializeNulls().registerTypeAdapter(User.class, new SerializerForUser()).create();

But somehow it is not working, I will still get below output when for example address is null:

{
  "name": "Adam",
  "id": 123,
  "address: null
}

can anyone give me some hints on what did I wrong here? Or it would be perfect if anyone can tell me if there is more straight forward/general way to achieve this(since I also want to use the same gson instance to ser/des other objects)? Any comments are appreciated.

1 Answers

Because you are using

Gson gson = new GsonBuilder().serializeNulls().create();

which shows null value.

To skip showing null, let's try

Gson gson = new Gson();

You can test here

public static void main(String[] args) {
    Gson yourGson = new GsonBuilder().serializeNulls().create(); // this is how you create your Gson object, which shows null value
    Address address = new Address(null, "New York", "US");
    User user = new User("Adam", 123, address);
    String userJson = yourGson.toJson(user);
    System.out.println(userJson);
    
    Gson newGson = new Gson(); // with this one, it doesn't show value
    System.out.println(newGson.toJson(user));
}

Update

I have tried to override the method serialize with a few times and it failed until I try #5

public class UserCustomSerializer implements JsonSerializer<User> {

    @Override
    public JsonElement serialize(User src, Type typeOfSrc, JsonSerializationContext context) {
        JsonObject obj = new JsonObject();

        if (src.name != null) {
            obj.addProperty("name", src.name);
        }
        obj.addProperty("id", src.id);

        if (src.address != null) {
            // try #1
            //JsonObject addressJsonObj = new JsonObject();
            //addressJsonObj.addProperty("street", src.address.street != null ? src.address.street : null);
            //addressJsonObj.addProperty("city", src.address.city != null ? src.address.city : null);
            //addressJsonObj.addProperty("country", src.address.country != null ? src.address.country : null);
            //obj.add("address", addressJsonObj);

            // try #2
            //Gson gson = new GsonBuilder().serializeNulls().create();
            //JsonElement jsonElement = gson.toJsonTree(src.address);
            //obj.add("address", jsonElement);

            // try #3
            //Gson gson2 = new GsonBuilder().serializeNulls().create();
            //obj.addProperty("address", gson2.toJson(src.address));

            // try #4
            //Gson gson = new GsonBuilder().serializeNulls().create();
            //JsonObject jsonObject = gson.toJsonTree(src.address).getAsJsonObject();
            //obj.add("address", jsonObject);
            
            // try #5
            JsonObject addressJsonObj = new JsonObject();
            addressJsonObj.addProperty("street", src.address.street != null ? src.address.street : "null");
            addressJsonObj.addProperty("city", src.address.city != null ? src.address.city : "null");
            addressJsonObj.addProperty("country", src.address.country != null ? src.address.country : "null");
            obj.add("address", addressJsonObj);
        }
        return obj;
    }
}

For try #3, I built the incorrect String.

For try #1, #2 and #4, I have the problem with the null value. I searched and found the reason and also the suggestion here

In a JSON "object" (aka dictionary), there are two ways to represent absent values: Either have no key/value pair at all, or have a key with the JSON value null.

So you either use .add with a proper value what will get translated to null when you build the JSON, or you don't have the .add call.

And my #5 approach is to check if the child node is null, I just add the string "null" literally and then I replace it when I build the json string

private String parseToGson(User user){
    Gson gson = new GsonBuilder().registerTypeAdapter(User.class, new UserCustomSerializer()).create();
    return gson.toJson(user).replace("\"null\"", "null");
}

Here are some test cases I defined

@Test
public void children_attribute_is_null() throws Exception {
    String expected = "{\"name\":\"Adam\"," 
                     + "\"id\":123," 
                     + "\"address\":{" 
                                    + "\""+ "street\":null," 
                                    + "\"city\":\"New York\"," 
                                    + "\"country\":\"US" 
                                + "\"}" 
                     + "}";
    Address address = new Address(null, "New York", "US");
    User user = new User("Adam", 123, address);
    assertEquals(expected, parseToGson(user));
    
    Gson g = new Gson(); 
    User usr = g.fromJson( parseToGson(user), User.class);
    assertEquals("Adam", usr.name);
    assertEquals(123, usr.id);
    assertEquals(null, usr.address.street);
    assertEquals("New York", usr.address.city);
    assertEquals("US", usr.address.country);
}

@Test
public void parent_attribute_is_null() throws Exception {
    String expected = "{\"name\":\"Adam\"," 
                     + "\"id\":123" + "}";
    User user = new User("Adam", 123, null);
    assertEquals(expected, parseToGson(user));
    
    Gson g = new Gson(); 
    User usr = g.fromJson( parseToGson(user), User.class);
    assertEquals("Adam", usr.name);
    assertEquals(123, usr.id);
    assertEquals(null, usr.address);
}

@Test
public void parent_attribute_and_children_attribute_are_null() throws Exception {
    String expected = "{\"id\":123," 
                     + "\"address\":{" 
                                + "\"street\":null," 
                                + "\"city\":\"New York\","
                                + "\"country\":\"US" 
                                + "\"}" 
                     + "}";
    Address address = new Address(null, "New York", "US");
    User user = new User(null, 123, address);
    assertEquals(expected, parseToGson(user));
    
    Gson g = new Gson(); 
    User usr = g.fromJson( parseToGson(user), User.class);
    assertEquals(null, usr.name);
    assertEquals(123, usr.id);
    assertEquals(null, usr.address.street);
    assertEquals("New York", usr.address.city);
    assertEquals("US", usr.address.country);
}

Update #2

Since the previous version is not a generic one, I would like to update the answer.

For generic, I created MyCustomSerializer as following

public class MyCustomSerializer<T> implements JsonSerializer<T> {

    private final Class<T> type;

    public MyCustomSerializer(Class<T> type) {
        this.type = type;
    }

    public Class<T> getMyType() {
        return this.type;
    }

    @Override
    public JsonElement serialize(T src, Type typeOfSrc, JsonSerializationContext context) {
        JsonObject obj = new JsonObject();
        try {
            Field[] declaredFields = this.type.getDeclaredFields();
            for (Field field : declaredFields) {
                Object object = field.get(src);
                if (object != null) {
                    // Here, we check for 4 types of JsonObject.addProperty
                    if (object instanceof String) {
                        obj.addProperty(field.getName(), (String) object);
                        continue;
                    }
                    if (object instanceof Number) {
                        obj.addProperty(field.getName(), (Number) object);
                        continue;
                    }
                    if (object instanceof Boolean) {
                        obj.addProperty(field.getName(), (Boolean) object);
                        continue;
                    }
                    if (object instanceof Character) {
                        obj.addProperty(field.getName(), (Character) object);
                        continue;
                    }
                    // This is where we check for other types
                    // The idea is if it is an object, we need to care its child object as well, so parse it into json string and replace the null value.
                    Gson gson = new GsonBuilder().serializeNulls().create();
                    String json = gson.toJson(object);
                    json = json.replace("null", "\"null\""); // We have to build the string first, then replace it with our special keys. In this case, I use the string "null"
                    JsonObject convertedObject = new Gson().fromJson(json, JsonObject.class); // Then convert it back to json object
                    obj.add(field.getName(), convertedObject);
                }
            }
        } catch (IllegalAccessException e) {
            e.printStackTrace();
        }
        return obj;
    }
}

The main idea is still the same as previous version but I made it to a generic one.

I also added some additional properties to test for the string this code builds with the results

{
   "id":123,
   "address":{
      "street":null,
      "city":"New York",
      "country":"US",
      "info":{
         "zipcode":null,
         "address2":"stackoverflow",
         "workPlaceAddress":{
            "street":null,
            "companyName":"google"
         }
      }
   }
}

To call this, we need to do

private String parseToGson(User user) {
    Gson gson = new GsonBuilder().registerTypeAdapter(User.class, new MyCustomSerializer<>(User.class)).create();
    return gson.toJson(user).replace("\"null\"", "null"); 
}

Update #3

Since you still concern about your solution, I tried to adapt it as well

public class YourSerializer <T> implements JsonSerializer<T>{
    
    private final Class<T> type;

    public YourSerializer(Class<T> type) {
        this.type = type;
    }

    public Class<T> getMyType() {
        return this.type;
    }

    @Override
    public JsonElement serialize(T src, Type typeOfSrc, JsonSerializationContext context) {
           Gson gson = new GsonBuilder().serializeNulls().create();
            JsonObject jsonObject = gson.toJsonTree(src).getAsJsonObject();
            
            Field[] declaredFields = this.type.getDeclaredFields();
            for (Field field : declaredFields) {
                try {
                    if(field.get(src) == null) {
                        jsonObject.remove(field.getName());
                    }
                } catch (IllegalAccessException e) {
                    e.printStackTrace();
                }
            }
            return jsonObject;
    }
}

The reason is you used serializeNulls() incorrectly which makes your output is incorrect. To correct it, you should registerTypeAdapter first to create your custom json, then you call serializeNulls

private String parseToGson(User user) {
    Gson gson = new GsonBuilder().registerTypeAdapter(User.class, new YourSerializer<>(User.class)).serializeNulls().create();  
    return gson.toJson(user);
}

I tested and got the same result with update#2

{
   "id":123,
   "address":{
      "street":null,
      "city":"New York",
      "country":"US",
      "info":{
         "zipcode":null,
         "address2":"aaa",
         "workPlaceAddress":{
            "street":null,
            "companyName":"google"
         }
      }
   }
}
Related