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

2

10

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

Consternation answered 1/3, 2017 at 4:34 Comment(0)
N
6

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}

Nyctaginaceous answered 1/3, 2017 at 7:31 Comment(2)
upvoted. the "Person" object is just a simplified example, so the DTO style solution might not be the cleanest way to go... probably something like personbuilder().withfax().build() something??Consternation
@Consternation Well, it depends, and I would go with DTOs to make it less error-prone. Note the trick: Gson can distinguish between PersonDto and PersonDtoWithFax upon the object actual type when invoking the toJson method. The builder pattern you mentioned is just another way of creating a DTO (I used constructors for the answer simplicity purposes only). If you want to use builder, it's all up to you, and you can easily create your own builder with withFax(...) returning a PersonDtoWithFaxBuilder` instance. But introducing builders usually brings some complexity. You decide.Nyctaginaceous
S
1

This can be solved using a custom TypeAdapterFactory and @JsonAdapter. The TypeAdapterFactory will be responsible for delegating to the actual adapter and omitting the field if its value is null. And @JsonAdapter will be used to only apply this to the specific fields where you want this behavior.

class ExcludeIfNullAdapterFactory implements TypeAdapterFactory {
    // Constructor is called by Gson using reflection
    public ExcludeIfNullAdapterFactory() {
    }

    @Override
    public <T> TypeAdapter<T> create(Gson gson, TypeToken<T> type) {
        // Note: Should rather use Gson.getDelegateAdapter(...), but that currently does not work
        // in combination with @JsonAdapter, see https://github.com/google/gson/issues/1028
        TypeAdapter<T> delegate = gson.getAdapter(type);

        return new TypeAdapter<T>() {
            @Override
            public T read(JsonReader in) throws IOException {
                return delegate.read(in);
            }

            @Override
            public void write(JsonWriter out, T value) throws IOException {
                if (value == null) {
                    // Temporarily disable serialization of null
                    boolean oldSerializeNulls = out.getSerializeNulls();
                    try {
                        out.setSerializeNulls(false);
                        out.nullValue();
                    } finally {
                        out.setSerializeNulls(oldSerializeNulls);
                    }
                } else {
                    delegate.write(out, value);
                }
            }
        };
    }
}

You then also have to use GsonBuilder.serializeNulls() so that by default null is serialized for all fields, except the ones you have annotated with @JsonAdapter.

Here is a full example:

class Person {
    String name;
    String phone;

    // Important: Must set `nullSafe = false` to not use default null handling
    @JsonAdapter(value = ExcludeIfNullAdapterFactory.class, nullSafe = false)
    String fax;
}
Gson gson = new GsonBuilder()
    .serializeNulls()
    .create();

Person p = new Person();
System.out.println(gson.toJson(p));

Here is an example for the opposite case, that is, by default exclude null values, except for certain fields where null must be serialized. The main difference is that you omit the call to GsonBuilder.serializeNulls(), and that the TypeAdapterFactory calls setSerializeNulls(true) instead of setSerializeNulls(false).

Sidoney answered 22/6, 2023 at 12:35 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.