How to serialize Optional<T> classes with Gson?
Asked Answered
C

6

27

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.

Cartel answered 28/8, 2012 at 14:20 Comment(3)
Optional shouldn't be used on properties or parameters, just in return types.Caulescent
@Caulescent 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 userEvangelina
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.Caulescent
A
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;
    }
}
Apc answered 28/8, 2012 at 17:33 Comment(7)
Do you need to register a new instance of GsonOptionalDeserializer for every type of Optional that you want to serialize?Cruces
@scompt.com No, it's just like here.Apc
Ah, I see. I ended up implementing TypeAdapterFactory and doing something similar to the Multiset example in the Javadocs.Cruces
Looks like during deserialization, this still doesn't distinguish between Optional.absent() and null since it returns a non-null Optional in all cases.Carpophagous
@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.Apc
Note that the encapsulation with an array is only needed when implementing JsonDeserializer (see also this Gson issue). The higher voted answer below uses TypeAdapter and therefore does not require this encapsulation and allows to write "myfield": null for an absent Optional.Tevere
@Tevere 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...Apc
S
24

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.

Sweepstakes answered 1/8, 2014 at 10:40 Comment(2)
read(...) is not consuming the null. It should call skipValue() or nextNull().Tevere
@Tevere I fixed this and providing TypeAdapter for List, here is my full code: gist.github.com/luochen1990/319de4c73f7269d197a2a3fe4523a1f7Kissiah
A
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;
    }
}
Apc answered 28/8, 2012 at 17:33 Comment(7)
Do you need to register a new instance of GsonOptionalDeserializer for every type of Optional that you want to serialize?Cruces
@scompt.com No, it's just like here.Apc
Ah, I see. I ended up implementing TypeAdapterFactory and doing something similar to the Multiset example in the Javadocs.Cruces
Looks like during deserialization, this still doesn't distinguish between Optional.absent() and null since it returns a non-null Optional in all cases.Carpophagous
@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.Apc
Note that the encapsulation with an array is only needed when implementing JsonDeserializer (see also this Gson issue). The higher voted answer below uses TypeAdapter and therefore does not require this encapsulation and allows to write "myfield": null for an absent Optional.Tevere
@Tevere 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...Apc
C
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());
    }
}
Conley answered 1/5, 2017 at 23:21 Comment(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).Discretional
M
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));
}
Minesweeper answered 18/12, 2018 at 6:42 Comment(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.Tevere
A
1

I dearly feel that its important for the reader which ends up in this thread to know at the time this answer is written, none of the solutions offered in the current answers actually solve the original requirement placed in the question.

TLDR Switch to Jackson

  • Requirement cannot be satisfied with Gson (at least not with the latest version in the time this answer is written that is 2.10.1).

  • All the offered solutions so far lead to the same result - upon parsing a json string, when an attribute is missing, even if the corresponding java entity class member type is Optional<?>, at the end of the de-serialization the member will end up referring a null and not an Optional.empty() as desired, leading to NullPointerException when retrieving the Optional member.

  • If you insist on using Gson, you'll have to manually address the scenarios where the optional attribute is absent within the entity class member getter i.e.

    public Optional<String> myOptionalString() {
         return this.myOptionalString == null ? Optional.empty() : myOptionalString
    }
    
  • Me personally, I ended up ditching Gson in favor of Jackson, which together with its Jdk8Module, solves the problem.

  • In Jackson, you'll create the ObjectMapper this way

    ObjectMapper mapper = new ObjectMapper().registerModule(new Jdk8Module());
    

    And then you'll parse your json string into an entity this way

    mapper.readValue(jsonWithOptionals, EntityWithOptionals.class)
    
Applecart answered 11/11, 2023 at 15:12 Comment(0)
E
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();
Excision answered 29/8, 2023 at 12:24 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.