Imagine I have an object that looks like this:
@Getter
@Setter
static class MyObject {
private String firstName;
private String lastName;
private long salary;
}
and then I have:
private static final String KEEP_ME = "$$$___KEEP_ME___$$$";
MyObject obj = new MyObject();
obj.setFirstName(KEEP_ME);
obj.setLastName(null);
obj.setSalary(1000);
I want to create a custom Serializer to remove any null and keep the fields that equals KEEP_ME with a null value, the result I expect should looks like this:
{"firstName":null,"salary":1000}
My Serializer looks like:
static class NullStringSerializer implements JsonSerializer<String> {
private static final String KEEP_ME = "$$$___KEEP_ME___$$$";
@Override
public JsonElement serialize(String src, Type typeOfSrc, JsonSerializationContext context) {
if (src == null) {
return JsonNull.INSTANCE;
}
if (KEEP_ME.equals(src)) {
return JsonNull.INSTANCE;
}
return context.serialize(src, typeOfSrc);
}
}
and
Gson gsonBuilder = new GsonBuilder()
.registerTypeAdapter(String.class, new NullStringSerializer())
.create();
gsonBuilder.toJson(obj);
but this give me:
{"salary":1000}
Any solution for this situation please ?
You can't do that with the custom value adapter like NullStringSerializer
, because the Gson internal DTO-classes like MyObject
type adapter's null-write policy allows either all nulls, or no nulls. Both controlled with a single flag set using GsonBuilder#serializeNulls()
. The ExclusionStrategy
interface won't help either because it's value-unaware. And that sucks.
I encountered your string marker approach in frameworks like the Spring Framework, where the markers were used in annotations that are not allowed to contain null
values, and that utilized actually the three-state approach just like your example suggests:
firstName
is always defined regardless of having a value or nulllastName
may be undefined because it might be encountered or not in the MyObject
serialized payloadAnd this is totally okay (and I don't see any reason of why the question has been downvoted). To my knowledge, the only two ways to implement the three-state approach is:
MyObject
(and that sucks too)KEEP_ME
Existential.value(...)
, Existential.nullValue()
and Existential.undefinedValue()
and then wrapping all three-state fields
Optional.of(...)
(for values), Optinal.empty()
(for null) and Java nulls for undefinedGsonBuilder#serializeNulls()
that sets the emit-nulls policy once invoked)null
token to the JSON output stream for Existential.nullValue()
or Optional.empty()
(or Optional.ofNullable(null)
).If you're okay with the second approach, it might be implemented like this:
@AllArgsConstructor(access = AccessLevel.PRIVATE)
public final class ExistentialTypeAdapter
implements TypeAdapterFactory {
@Getter
private static final TypeAdapterFactory instance = new ExistentialTypeAdapter();
@Override
@Nullable
public <T> TypeAdapter<T> create(final Gson gson, final TypeToken<T> typeToken) {
if ( !Optional.class.isAssignableFrom(typeToken.getRawType()) ) {
return null;
}
final ParameterizedType parameterizedType = (ParameterizedType) typeToken.getType();
final Type optionalTypeArgument = parameterizedType.getActualTypeArguments()[0];
final TypeAdapter<?> backingTypeAdapter = gson.getDelegateAdapter(this, TypeToken.get(optionalTypeArgument));
@SuppressWarnings("unchecked")
final TypeAdapter<T> adapter = (TypeAdapter<T>) new Adapter<>(backingTypeAdapter);
return adapter;
}
@AllArgsConstructor(access = AccessLevel.PRIVATE)
private static final class Adapter<T>
extends TypeAdapter<Optional<T>> {
private final TypeAdapter<T> backingTypeAdapter;
@Override
public Optional<T> read(final JsonReader in) {
throw new UnsupportedOperationException();
}
@Override
public void write(final JsonWriter out, final Optional<T> value)
throws IOException {
if ( value.isEmpty() ) {
out.jsonValue("null"); // NOTE (!) it's NOT a string, but emits the `null` token
return;
}
backingTypeAdapter.write(out, value.get());
}
}
}
public final class ExistentialTypeAdapterTest {
@Getter
@Setter
private static class MyObject {
@Nullable
@SuppressWarnings("OptionalUsedAsFieldOrParameterType")
private Optional<String> firstName;
@Nullable
private String lastName;
private long salary;
}
private static final Gson gson = new GsonBuilder()
// DO NOT uncomment: .serializeNulls()
.registerTypeAdapterFactory(ExistentialTypeAdapter.getInstance())
.create();
@Test
public void test() {
final MyObject obj = new MyObject();
obj.setFirstName(Optional.empty()); // this would stand for "null" (the has-property state with explicit null value)
obj.setLastName(null); // this would stand for "undefined" (the has-no-property state in the result JSON object)
obj.setSalary(1000);
System.out.println(gson.toJson(obj));
}
}
The output:
{"firstName":null,"salary":1000}
Also, I do recommend using type adapters instead for performance reasons.