Search code examples
androidgsonretrofitokhttp

Retrofit + Gson: calculate a field dynamically or call constructor


When a response comes with Retrofit mapped to a model with Gson I like to calculate a field instead if setting the received value.

When the response comes back for an accessToken, it looks like this:

public class UserTokenResponse {
    @SerializedName("access_token")
    @Expose
    private String accessToken;          //v^1.1#i^1#r...
    @SerializedName("expires_in")
    @Expose
    private long accessTokenExpiresIn;   //7200
    //Constructor... setter ... getter
}

The problem here is the field accessTokenExpiresIn 7200. I like to have it calculated like this:

public UserTokenResponse (...) {        //Constructor
    this.accessTokenExpiresIn = System.currentTimeMillis() + 
    (accessTokenExpiresIn * 1000L);
}

So that it will become a Unix timestamp with the actual expires millisecond. But as the constructor is not called the field will be 7200.


Solution

  • The easiest solution to this is probably to use Gson's @JsonAdapter on the field and to specify a type adapter which performs this calculation. The adapter could then for example look like this:

    class ExpirationAdapter extends TypeAdapter<Long> {
        // No-args constructor called by Gson
        public ExpirationAdapter() { }
    
        @Override
        public void write(JsonWriter out, Long value) throws IOException {
            throw new UnsupportedOperationException();
        }
    
        @Override
        public Long read(JsonReader in) throws IOException {
            long expiration = in.nextLong();
            return System.currentTimeMillis() + (expiration * 1000L);
        }
    }
    

    And your UserTokenResponse class would look like this:

    class UserTokenResponse {
        @SerializedName("expires_in")
        @Expose
        @JsonAdapter(ExpirationAdapter.class)
        private long accessTokenExpiresIn;
    
        // ...
    }
    

    You could even change the field type to java.time.Instant, which might be easier and less error-prone to work with than just a long, and adjust the adapter accordingly:

    class ExpirationAdapter extends TypeAdapter<Instant> {
        // No-args constructor called by Gson
        public ExpirationAdapter() { }
    
        @Override
        public void write(JsonWriter out, Instant value) throws IOException {
            throw new UnsupportedOperationException();
        }
    
        @Override
        public Instant read(JsonReader in) throws IOException {
            long expiration = in.nextLong();
            return Instant.now().plusSeconds(expiration);
        }
    }
    

    In general there are also the following other solutions, but compared to using @JsonAdapter they have some big drawbacks:

    • Registering an adapter for the type of the field
      Not really feasible because the field has type long and you would then override Gson's default handling for long values, even for completely unrelated fields where this expiration timestamp conversion is not desired.
    • Registering an adapter for the declaring type of the field
      Registering a type for UserTokenResponse would be possible, but you would have to manually perform the deserialization of the class, and would have to handle annotations such as @SerializedName manually as well. Or you would have to write a TypeAdapterFactory which performs the default deserialization and then afterwards adjusts the field value, but that would for example not allow changing the field type to java.time.Instant.

    Also note that Gson does not call setter methods or constructors, it directly sets the field values. So if possible you should always provide a no-args constructor, otherwise Gson tries to create an instance without calling a constructor, which can lead to confusing errors, see GsonBuilder.disableJdkUnsafe() for more information.