Is there a generic way to tell Gson to not write empty string?
I strongly dislike having to implement a TypeAdapter which handles every field as the answer here somewhat suggests.
Is there a generic way to tell Gson to not write empty string?
I strongly dislike having to implement a TypeAdapter which handles every field as the answer here somewhat suggests.
Sort of. As far as I know, Gson does not let you control over object fields much, and the only control know to me you can have it @JsonAdapter
. For example,
import com.google.gson.annotations.JsonAdapter;
final class Pack {
@JsonAdapter(EmptyStringTypeAdapter.class)
final String foo;
@JsonAdapter(EmptyStringTypeAdapter.class)
final String bar;
private Pack(final String foo, final String bar) {
this.foo = foo;
this.bar = bar;
}
static Pack of(final String foo, final String bar) {
return new Pack(foo, bar);
}
@Override
public String toString() {
return foo + " " + bar;
}
}
Despite it might look tedious for someone, it gives your total control over your data transfer objects giving you a choice on what to do with this or that string. The example type adapter may be as follows:
final class EmptyStringTypeAdapter
extends TypeAdapter<String> {
private EmptyStringTypeAdapter() {
}
@Override
@SuppressWarnings("resource")
public void write(final JsonWriter jsonWriter, @Nullable final String s)
throws IOException {
if ( s == null || s.isEmpty() ) {
jsonWriter.nullValue();
} else {
jsonWriter.value(s);
}
}
@Override
@Nonnull
@SuppressWarnings("EnumSwitchStatementWhichMissesCases")
public String read(final JsonReader jsonReader)
throws IOException {
final JsonToken token = jsonReader.peek();
switch ( token ) {
case NULL:
return "";
case STRING:
return jsonReader.nextString();
default:
throw new IllegalStateException("Unexpected token: " + token);
}
}
}
One caveat here is that it cannot restore empty strings from nulls (and you're caught with irreversible conversion here unfortunately), so you might also want to take a look at https://github.com/google/gson/blob/master/extras/src/main/java/com/google/gson/typeadapters/PostConstructAdapterFactory.java to restore @JsonAdapter(EmptyStringTypeAdapter.class)
-annotated fields on read. Example test:
private static final Gson gson = new Gson();
private static final Type listOfStringType = new TypeToken<List<String>>() {
}.getType();
public static void main(final String... args) {
// Single elements
ImmutableList.of(Pack.of("", ""), Pack.of("foo", ""), Pack.of("", "bar"), Pack.of("foo", "bar"))
.stream()
.peek(pack -> System.out.println("Pack before: " + pack))
.map(gson::toJson)
.peek(json -> System.out.println("JSON: " + json))
.map(json -> gson.fromJson(json, Pack.class))
.peek(pack -> System.out.println("Pack after: " + pack))
.forEach(pack -> System.out.println());
// Multiple elements
final List<String> stringsBefore = ImmutableList.of("", "foo", "bar");
System.out.println(stringsBefore);
final String stringsJson = gson.toJson(stringsBefore, listOfStringType);
System.out.println(stringsJson);
final List<String> stringsAfter = gson.fromJson(stringsJson, listOfStringType);
System.out.println(stringsAfter);
}
Output:
Pack before:
JSON: {}
Pack after: null null -- [!] not "" '"Pack before: foo
JSON: {"foo":"foo"}
Pack after: foo null -- [!] not foo ""Pack before: bar
JSON: {"bar":"bar"}
Pack after: null bar -- [!] not "" barPack before: foo bar
JSON: {"foo":"foo","bar":"bar"}
Pack after: foo bar[, foo, bar]
["","foo","bar"]
[, foo, bar]
However, I don't think that writing sophisticated (de)serialization strategies is a good choice, and you probably might be interested in redesigning your DTOs and data (de)serialization. Moreover, ""
is a value, whilst null
is not -- I would never mix them and I would revise why your system is designed that way (it really looks like an empty/null values mix issue).
Thanks to @Lyubomyr for his answer but I found a solution that fits our use case better:
If we set all empty strings and objects to null the leftover JSON after serializing only contains nodes with actual data:
/**
* convert object to json
*/
public String toJson(Object obj) {
// Convert emtpy string and objects to null so we don't serialze them
setEmtpyStringsAndObjectsToNull(obj);
return gson.toJson(obj);
}
/**
* Sets all empty strings and objects (all fields null) including sets to null.
*
* @param obj any object
*/
public void setEmtpyStringsAndObjectsToNull(Object obj) {
for (Field field : obj.getClass().getDeclaredFields()) {
field.setAccessible(true);
try {
Object fieldObj = field.get(obj);
if (fieldObj != null) {
Class fieldType = field.getType();
if (fieldType.isAssignableFrom(String.class)) {
if(fieldObj.equals("")) {
field.set(obj, null);
}
} else if (fieldType.isAssignableFrom(Set.class)) {
for (Object item : (Set) fieldObj) {
setEmtpyStringsAndObjectsToNull(item);
}
boolean setFielToNull = true;
for (Object item : (Set) field.get(obj)) {
if(item != null) {
setFielToNull = false;
break;
}
}
if(setFielToNull) {
setFieldToNull(obj, field);
}
} else if (!isPrimitiveOrWrapper(fieldType)) {
setEmtpyStringsAndObjectsToNull(fieldObj);
boolean setFielToNull = true;
for (Field f : fieldObj.getClass().getDeclaredFields()) {
f.setAccessible(true);
if(f.get(fieldObj) != null) {
setFielToNull = false;
break;
}
}
if(setFielToNull) {
setFieldToNull(obj, field);
}
}
}
} catch (IllegalAccessException e) {
System.err.println("Error while setting empty string or object to null: " + e.getMessage());
}
}
}
private void setFieldToNull(Object obj, Field field) throws IllegalAccessException {
if(!Modifier.isFinal(field.getModifiers())) {
field.set(obj, null);
}
}
private boolean isPrimitiveOrWrapper(Class fieldType) {
return fieldType.isPrimitive()
|| fieldType.isAssignableFrom(Integer.class)
|| fieldType.isAssignableFrom(Boolean.class)
|| fieldType.isAssignableFrom(Byte.class)
|| fieldType.isAssignableFrom(Character.class)
|| fieldType.isAssignableFrom(Float.class)
|| fieldType.isAssignableFrom(Long.class)
|| fieldType.isAssignableFrom(Double.class)
|| fieldType.isAssignableFrom(Short.class);
}
Performance whise this runs reasonably quick. If you have a lot of empty fields this saves time (and space) when serializing and sending/writing to DB.
Since you are interested in omitting empty strings during serialization, you can only implement JsonSerializer instead of full TypeAdapter.
public class EmptyStringSerializer implements JsonSerializer<String> {
@Override
public JsonElement serialize(String src, Type typeOfSrc, JsonSerializationContext context) {
if (src == null || src.isEmpty())
return JsonNull.INSTANCE;
return new JsonPrimitive(src);
}
}
And then use it same as a TypeAdapter:
Gson gson = new GsonBuilder()
.registerTypeAdapter(String.class, new EmptyStringSerializer())
.create();
(This approach relies on omitting null values from serialization.)