How do I treat empty Strings as null objects with GSON?
Asked Answered
L

1

7

I'm retrieving comments from the Reddit API. The model is threaded such that each Comment can internally have a List of Comments, named replies. Here's an example of how a JSON response would look:

[
   {
      "kind":"Listing",
      "data":{
         "children":[
            {
               "data":{
                  "body":"comment",
                  "replies":{
                     "kind":"Listing",
                     "data":{
                        "children":[
                           {
                              "data":{
                                 "body":"reply to comment",
                                 "replies":""
                              }
                           }
                        ]
                     }
                  }
               }
            }
         ]
      }
   }
]

Here is how I model this with POJOs. The response above would be considered a List of CommentListings.

public class CommentListing {
    @SerializedName("data")
    private CommentListingData data;
}

public final class CommentListingData {
    @SerializedName("children")
    private List<Comment> comments;
}

public class Comment {
    @SerializedName("data")
    private CommentData data;
}

public class CommentData {
    @SerializedName("body")
    private String body;

    @SerializedName("replies")
    private CommentListing replies;
}

Note how the bottom level CommentData POJO refers to another CommentListing called "replies".

This model works until GSON reaches the last child CommentData where there are no replies. Rather than providing a null, the API is providing an empty String. Naturally, this causes a GSON exception where it expects an object but finds a String:

"replies":""

Expected BEGIN_OBJECT but was STRING

I attempted to create a custom deserializer on the CommentData class, but due to the recursive nature of the model it seems not to reach the bottom levels of the model. I imagine this is because I'm using a separate GSON instance to complete deserialization.

@Singleton
@Provides
Gson provideGson() {
    Gson gson = new Gson();
    return new GsonBuilder()
            .registerTypeAdapter(CommentData.class, new JsonDeserializer<CommentData>() {
                @Override
                public CommentData deserialize(JsonElement json, Type typeOfT, JsonDeserializationContext context) throws JsonParseException {
                    JsonObject commentDataJsonObj = json.getAsJsonObject();
                    JsonElement repliesJsonObj = commentDataJsonObj.get("replies");
                    if (repliesJsonObj != null && repliesJsonObj.isJsonPrimitive()) {
                        commentDataJsonObj.remove("replies");
                    }

                    return gson.fromJson(commentDataJsonObj, CommentData.class);
                }
            })
            .serializeNulls()
            .create();
}

How can I force GSON to return a null instead of a String so that it doesn't try to force a String into my POJO? Or if that's not possible, manually reconcile the data issue? Please let me know if you need additional context or information. Thanks.

Lawanda answered 15/2, 2018 at 5:12 Comment(0)
T
19

In general your code looks good, but I would recommend a few things:

  • Your type adapters should not capture Gson instances from outside. Type adapter factories (TypeAdapterFactory) are designed for this purpose. Also, in JSON serializers and deserializers you can implicitly refer it through JsonSerializationContext and JsonDeserializationContext respectively (this avoids infinite recursion in some cases).
  • Avoid modification JSON objects in memory as much as possible: serializers and deserializers are just a sort of pipes and should not bring you surprises with modified objects.
  • You can implement a generic "empty string as a null" type deserializer and annotate each "bad" field that requires this kind of deserialization strategy. You might consider it's tedious, but it gives you total control wherever you need it (I don't know if Reddit API has some more quirks like this).
public final class EmptyStringAsNullTypeAdapter<T>
        implements JsonDeserializer<T> {

    // Let Gson instantiate it itself
    private EmptyStringAsNullTypeAdapter() {
    }

    @Override
    public T deserialize(final JsonElement jsonElement, final Type type, final JsonDeserializationContext context)
            throws JsonParseException {
        if ( jsonElement.isJsonPrimitive() ) {
            final JsonPrimitive jsonPrimitive = jsonElement.getAsJsonPrimitive();
            if ( jsonPrimitive.isString() && jsonPrimitive.getAsString().isEmpty() ) {
                return null;
            }
        }
        return context.deserialize(jsonElement, type);
    }

}

And then just annotate the replies field:

@SerializedName("replies")
@JsonAdapter(EmptyStringAsNullTypeAdapter.class)
private CommentListing replies;
Tibetoburman answered 15/2, 2018 at 12:10 Comment(6)
Brilliant, thanks Lyubomyr! Very clean and flexible solution.Lawanda
tahnk you , you saved my dayGraceless
I got troubled that why my code print strange. My class looks like: class Person { @JsonAdapter(EmptyStringAsNullTypeAdapter::class) String name; } When I try to print this class as string, I got this line: {"name":{"value":[],"coder":0,"hash":0}}Portiaportico
what is the SerializedName annotation for?Umpire
I was trying with sample model and I also got the same thining, my code prints strange. My class looks like: class Person { @JsonAdapter(EmptyStringAsNullTypeAdapter::class) String name; } When I try to print this class as string, I got this line: {"name":{"value":[],"coder":0,"hash":0}}Suter
@MaazPatel & @lovefish. I encountered the same issue and fix it by implementing a serializer together with the deserializer public final class EmptyStringAsNullTypeAdapter<T> implements JsonDeserializer<T>, JsonSerializer<T> { ... @Override public JsonElement serialize(T src, Type typeOfSrc, JsonSerializationContext context) { return context.serialize(src, typeOfSrc); } }Funeral

© 2022 - 2024 — McMap. All rights reserved.