5

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.

2
  • github.com/google/gson/issues/512 maybe this could help? Commented Jan 25, 2018 at 7:51
  • Thanks but I already found that and I don't quite see how to implement it or how it loops over all fields. Commented Jan 25, 2018 at 7:57

3 Answers 3

3

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 "" bar

Pack 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).

Sign up to request clarification or add additional context in comments.

1 Comment

While I don't disagree with you that it is an underlying issue, we have a performance problem now with new requirements and we don't have time to redesign everything. ¯_(ツ)_/¯
3

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.

Comments

0

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.)

Comments

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.