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}