0

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.

S. Gissel
  • 1,788
  • 2
  • 15
  • 32

2 Answers2

1

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.

Marcono1234
  • 5,856
  • 1
  • 25
  • 43
  • This is great! Your answer solves my problem to the full extend. I like the extra information for only having an empty constructor. – S. Gissel Feb 22 '23 at 08:52
0

You can try using a custom GSON deserializer like explained in this answer. The last link shows how to create a custom GSON factory with a custom deserializer and how to attach it to a Retrofit instance.

Basically you would change the value of the token response inside the deserializer method.

Agosto
  • 13
  • 2
  • This answer could be improved by supplying details in the post itself, in case the link supplied no longer works in the future. – Greg H Feb 25 '23 at 21:12