Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Gson serialization: exclude SOME fields if null but not all of them

Tags:

java

gson

Given a POJO:

class Person {
    @Expose
    String name;
    @Expose
    String phone;
    @Expose
    String fax;
}

I want "phone" to be serialized at all times, but "fax" only when it is not null. So given a Person "John" with no phone or fax:

Current:

{ "name": "John", "phone": null, "fax": null }

What I need:

{ "name": "John", "phone": null }

Is there something like:

@Expose (serialize_if_null = false)

transient doesn't work because I still want it serialized if it has a value.

Using ExclusionStrategy, I can define the field, but I cannot seem to find a way to get the value.

Thanks

like image 704
sikidhart Avatar asked Mar 01 '17 04:03

sikidhart


People also ask

Does Gson ignore null fields?

The default behavior that is implemented in Gson is that null object fields are ignored. For example, if in Employee object, we do not specify the email (i.e. email is null ) then email will not be part of serialized JSON output. Gson ignores null fields because this behavior allows a more compact JSON output format.

How do I exclude a field from serialization?

If there are fields in Java objects that do not wish to be serialized, we can use the @JsonIgnore annotation in the Jackson library. The @JsonIgnore can be used at the field level, for ignoring fields during the serialization and deserialization.

Does Gson ignore extra fields?

As you can see, Gson will ignore the unknown fields and simply match the fields that it's able to.

Does Gson ignore transient fields?

Gson serializer will ignore every field declared as transient: String jsonString = new Gson().


1 Answers

Using ExclusionStrategy, I can define the field, but I cannot seem to find a way to get the value.

Yes, it does not provide a way of determining the current field value. This is because of how Gson ReflectiveTypeAdapterFactory works internally (the BoundField.serialized is final and only resolved once):

@Override public boolean writeField(Object value) throws IOException, IllegalAccessException {
  if (!serialized) return false;
  Object fieldValue = field.get(value);
  return fieldValue != value; // avoid recursion for example for Throwable.cause
}
for (BoundField boundField : boundFields.values()) {
  if (boundField.writeField(value)) {
    out.name(boundField.name);
    boundField.write(out, value);
  }
}

This behavior cannot be changed, but I believe it's a good design choice to segregate application objects and their serialized representations (see the Data Transfer Object pattern) in order not to mix concepts and make applicaiton components loosely coupled (migrating from Gson someday would take only modifications for the respective DTO classes only).

If you're fine with having DTOs introduced to your application, then you could create separate DTO classes for both scenarios: preserving phone and discarding fax depending on the fax field value.

class PersonDto {
    @Expose String name;
    @Expose String phone;
    PersonDto(final Person person) {
        name = person.name;
        phone = person.phone;
    }
}
class PersonDtoWithFax extends PersonDto {
    @Expose String fax;
    PersonDtoWithFax(final Person person) {
        super(person);
        fax = person.fax;
    }
}

In this case the serialization is straight-forward:

final Gson gson = new GsonBuilder()
        .serializeNulls()
        .create();
final Person person = new Person();
person.name = "John";
final PersonDto personDto = person.fax == null
        ? new PersonDto(person)
        : new PersonDtoWithFax(person);
System.out.println(gson.toJson(personDto));

If you don't want to introduce the segregated DTO concept per se, you probably might want to implement a custom serializer that is somewhat more complicated in implementation and probably somewhat error-prone due to property names hardcoding (but you can have good tests, of course, or extract the names from java.lang.reflect.Field instances).

final class SpecialJsonSerializer<T>
        implements JsonSerializer<T> {

    private final Gson gson; // Unfortunately, Gson does not provide much from JsonSerialiationContext, so we have to get it ourselves
    private final Iterable<String> excludeIfNull;

    private SpecialJsonSerializer(final Gson gson, final Iterable<String> excludeIfNull) {
        this.gson = gson;
        this.excludeIfNull = excludeIfNull;
    }

    static <T> JsonSerializer<T> getSpecialJsonSerializer(final Gson gson, final Iterable<String> excludeIfNull) {
        return new SpecialJsonSerializer<>(gson, excludeIfNull);
    }

    @Override
    public JsonElement serialize(final T object, final Type type, final JsonSerializationContext context) {
        // context.serialize(person, type) cannot work due to infinite recursive serialization
        // therefore the backing Gson instance is used
        final JsonObject jsonObject = gson.toJsonTree(object, type).getAsJsonObject();
        for ( final String propertyName : excludeIfNull ) {
            final JsonElement property = jsonObject.get(propertyName);
            if ( property != null && property.isJsonNull() ) {
                jsonObject.remove(propertyName);
            }
        }
        return jsonObject;
    }

}

I'm not really sure, but I think that creating JSON trees for serialization purposes rather than using DTOs may be slightly more expensive from the memory consumption point of view (at least because of more complicated JsonElement structure).

// Both Gson instances must have serializeNulls()
final Gson gson = new GsonBuilder()
        .serializeNulls()
        .create();
final Gson gsonWrapper = new GsonBuilder()
        .serializeNulls()
        .registerTypeAdapter(Person.class, getSpecialJsonSerializer(gson, singletonList("fax")))
        .create();
final Person person = new Person();
person.name = "John";
System.out.println(gsonWrapper.toJson(person));

Both solutions output:

{"name":"John","phone":null}

like image 186
Lyubomyr Shaydariv Avatar answered Oct 18 '22 04:10

Lyubomyr Shaydariv