26

I have an object with the following attributes.

private final String messageBundle;
private final List<String> messageParams;
private final String actionBundle;
private final Map<String, String> data;
private final Optional<Pair<Integer,TimeUnit>> ttl;
private final Optional<Integer> badgeNumber;
private final Optional<String> collapseKey;

The object is in a library, i would rather not modify it just for serialization purpose, and would like to avoid the cost of creating another DTO.

How can i serialize / unserialize Optional attributes? Optional doesn't have a default constructor (neither apache commons Pair), but i can't use the InstanceCreator, and don't really understand how to create a TypeAdapter that would simply delegate the serialization to the underlying Optional content.

Sebastien Lorber
  • 89,644
  • 67
  • 288
  • 419
  • 1
    Optional shouldn't be used on properties or parameters, just in return types. – sargue Nov 02 '16 at 14:26
  • @sargue I think there are valid scenarios when this may be an option. For example you are writing a library for an api service with optional params. So you have to distinguish default value from value set by user – CAMOBAP Jun 16 '23 at 06:07
  • I'm not saying it, the javadocs says it: Optional is primarily intended for use as a method return type where there is a clear need to represent "no result," and where using null is likely to cause errors. – sargue Jun 16 '23 at 08:29

5 Answers5

23

After several hours of gooling and coding - there is my version:

public class OptionalTypeAdapter<E> extends TypeAdapter<Optional<E>> {

    public static final TypeAdapterFactory FACTORY = new TypeAdapterFactory() {
        @Override
        public <T> TypeAdapter<T> create(Gson gson, TypeToken<T> type) {
            Class<T> rawType = (Class<T>) type.getRawType();
            if (rawType != Optional.class) {
                return null;
            }
            final ParameterizedType parameterizedType = (ParameterizedType) type.getType();
            final Type actualType = parameterizedType.getActualTypeArguments()[0];
            final TypeAdapter<?> adapter = gson.getAdapter(TypeToken.get(actualType));
            return new OptionalTypeAdapter(adapter);
        }
    };
    private final TypeAdapter<E> adapter;

    public OptionalTypeAdapter(TypeAdapter<E> adapter) {

        this.adapter = adapter;
    }

    @Override
    public void write(JsonWriter out, Optional<E> value) throws IOException {
        if(value.isPresent()){
            adapter.write(out, value.get());
        } else {
            out.nullValue();
        }
    }

    @Override
    public Optional<E> read(JsonReader in) throws IOException {
        final JsonToken peek = in.peek();
        if(peek != JsonToken.NULL){
            return Optional.ofNullable(adapter.read(in));
        }

        in.nextNull();
        return Optional.empty();
    }

}

You can simple registered it with GsonBuilder like this:

instance.registerTypeAdapterFactory(OptionalTypeAdapter.FACTORY)

Please keep attention that Gson does not set values to your class field if field does not present in json. So you need to set default value Optional.empty() in your entity.

Marcono1234
  • 5,856
  • 1
  • 25
  • 43
  • 2
    `read(...)` is not consuming the `null`. It should call `skipValue()` or `nextNull()`. – Marcono1234 May 20 '20 at 09:18
  • 1
    @Marcono1234 I fixed this and providing TypeAdapter for List, here is my full code: https://gist.github.com/luochen1990/319de4c73f7269d197a2a3fe4523a1f7 – luochen1990 Jun 17 '20 at 05:05
10

The solution by Ilya ignores type parameters, so it can't really work in the general case. My solution is rather complicated, because of the need to distinguish between null and Optional.absent() -- otherwise you could strip away the encapsulation as a list.

public class GsonOptionalDeserializer<T>
implements JsonSerializer<Optional<T>>, JsonDeserializer<Optional<T>> {

    @Override
    public Optional<T> deserialize(JsonElement json, Type typeOfT, JsonDeserializationContext context)
            throws JsonParseException {
        final JsonArray asJsonArray = json.getAsJsonArray();
        final JsonElement jsonElement = asJsonArray.get(0);
        final T value = context.deserialize(jsonElement, ((ParameterizedType) typeOfT).getActualTypeArguments()[0]);
        return Optional.fromNullable(value);
    }

    @Override
    public JsonElement serialize(Optional<T> src, Type typeOfSrc, JsonSerializationContext context) {
        final JsonElement element = context.serialize(src.orNull());
        final JsonArray result = new JsonArray();
        result.add(element);
        return result;
    }
}
maaartinus
  • 44,714
  • 32
  • 161
  • 320
  • Do you need to register a new instance of `GsonOptionalDeserializer` for every type of `Optional` that you want to serialize? – Edward Dale May 28 '14 at 04:17
  • 1
    @scompt.com No, it's just like [here](http://stackoverflow.com/a/7773201/581205). – maaartinus May 28 '14 at 06:42
  • Ah, I see. I ended up implementing `TypeAdapterFactory` and doing something similar to the [Multiset example](http://google-gson.googlecode.com/svn/trunk/gson/docs/javadocs/com/google/gson/TypeAdapterFactory.html) in the Javadocs. – Edward Dale May 28 '14 at 07:08
  • Looks like during deserialization, this still doesn't distinguish between `Optional.absent()` and `null` since it returns a non-null `Optional` in all cases. – Jack Edmonds Sep 07 '18 at 15:17
  • @JackEdmonds Indeed and it can't as it'd need different representations for `Optional.absent()` and `null`. You could solve it by using a single-element array instead (as `Optional` is an up-to-one-element collection), but OTOH, `Optional` is nothing but exploded `@Nullable` and having `@Nullable Optional` is just plain wrong. – maaartinus Sep 08 '18 at 21:41
  • Note that the encapsulation with an array is only needed when implementing `JsonDeserializer` (see also [this Gson issue](https://github.com/google/gson/issues/1697)). The higher voted answer below uses `TypeAdapter` and therefore does not require this encapsulation and allows to write `"myfield": null` for an absent Optional. – Marcono1234 May 20 '20 at 09:34
  • @Marcono1234 But someone may expect `"myfield": null` to be transformed into `null` (sure, a nullable Optional is stupid..., stupid like.... like Optional itself :D). As I wrote... *"otherwise you could strip away the encapsulation as a list."* I'd bet, my solution could do this... what I don't understand is why the other answer is much longer. I didn't touch Gson for ages... – maaartinus May 21 '20 at 03:36
4

Just as an addition to maaartinus solution, the version without the encapsulating list, where Optional.absent is simply serialized as null:

public class GsonOptionalDeserializer<T> implements JsonSerializer<Optional<T>>, JsonDeserializer<Optional<T>> {
    @Override
    public Optional<T> deserialize(JsonElement json, Type typeOfT, JsonDeserializationContext context)
        throws JsonParseException {
        final T value = context.deserialize(json, ((ParameterizedType) typeOfT).getActualTypeArguments()[0]);
        return Optional.fromNullable(value);
    }

    @Override
    public JsonElement serialize(Optional<T> src, Type typeOfSrc, JsonSerializationContext context) {
        return context.serialize(src.orNull());
    }
}
Edgar Asatryan
  • 687
  • 9
  • 13
Till - Appviewer.io
  • 4,529
  • 1
  • 31
  • 35
  • 1
    This is better than the answer. The JsonArray is not required. Note that this answer is about Guava optionals. For Java8 optionals, use `Optional.ofNullable(value)` and `src.orElse(null)`. – timh Jul 04 '17 at 09:30
1

I'll add to Anton Onikiychuk's answer

@Override
public Optional<E> read(JsonReader in) throws IOException {
    JsonToken peek = in.peek();
    if (peek == JsonToken.NULL) {
        in.nextNull(); // consuming JSON null
        return Optional.empty();
    }

    return Optional.ofNullable(adapter.read(in));
}
Edgar Asatryan
  • 687
  • 9
  • 13
  • 1
    That's a good and important point! Though it would probably have been better to add that as comment or propose it as edit. I have proposed an edit solving this now. – Marcono1234 May 20 '20 at 09:36
0

I've encountered a similar challenge when trying to create a custom Gson TypeAdapter for optional types like java.util.Optional. Despite searching extensively and reviewing existing answers, I couldn't find a solution that precisely addressed this issue.

However, after thorough experimentation, I've managed to create a custom TypeAdapter that should work for your scenario:

/**
 * TypeAdapter to manage nullable fields with Optional approach
 */
public static class OptionalTypeAdapter<E> implements JsonSerializer<Optional<E>>, JsonDeserializer<Optional<E>> {

    @Override
    public JsonElement serialize(Optional<E> src, Type typeOfSrc, JsonSerializationContext context) {
        if (src.isPresent()) {
            return context.serialize(src.get());
        } else {
            return JsonNull.INSTANCE;
        }
    }

    @Override
    public Optional<E> deserialize(JsonElement json, Type typeOfT, JsonDeserializationContext context)
            throws JsonParseException {
        if (json.isJsonNull()) {
            return Optional.empty();
        } else {
            E value = context.deserialize(json, ((ParameterizedType) typeOfT).getActualTypeArguments()[0]);
            return Optional.ofNullable(value);
        }
    }
}

This custom OptionalTypeAdapter should allow Gson to handle optional types appropriately in serialization and deserialization.

Instance a Gson object to handle JSON is now possible through this code:

new GsonBuilder()
.registerTypeAdapter(Optional.class, new OptionalTypeAdapter<>())
.create();
ncla
  • 1