In my previous post I provided a simple example of how to use Retrofit to define, create and use a REST API client. Even in that simple example the issue of how to deal with different responses to a request came up. That is, where the response we receive does not conform to the strongly typed response we expected (or hoped for). Here’s how we deal with that, in a strongly typed way.
You may recall from my previous post that we defined a POPO type for the “happy” response to a request to the token end-point for logging in with the resource owner password flow.
TokenResponse = public class public access_token: String; refresh_token: String; token_type: String; expires_in: Integer; username: String; [SerializedName('as:client_id')] client_id: String; [SerializedName('.issued')] issuedUTC: String; [SerializedName('.expires')] expiresUTC: String; end;
This POPO represents the response to a successful login.
In the handler for our response, we could access the response body in a strongly typed fashion using exactly this POPO class:
method LoginActivity.onResponse(request: Call<TokenResponse>; response: Response<TokenResponse>); begin if response.code = 200 then begin // response.body is a TokenResponse. i.e. an instance of the POPO we expected var greeting := "Hello " + response.body.username; end else ... end;
But what if our request was unsuccessful ? By which I mean we provided an invalid username or password or an incorrect client id or secret etc ?
My back-end is an ASP.NET MVC WebApi in which I have implemented an OAuth 2.0 token provider. A response to an invalid request is made by simply setting some error details on the OAuth Client Authentication context. Here’s an excerpt to illustrate:
method AuthorizationServerProvider.GrantResourceOwnerCredentials(context: OAuthGrantResourceOwnerCredentialsContext): Task; begin // ... details omitted for brevity ... if user = nil then begin context.SetError('invalid_grant', 'The user name or password is invalid'); exit; end; // ... details omitted for brevity ... end;
All similar errors are set in the same way. In the event of a request completing in an error state, the .NET Oauth implementation will produce a JSON response similar to:
{ "error": "invalid_grant", "error_description": "The user name or password is invalid', "error_uri": null }
So, in addition to the TokenResponse, I need another POPO. Let’s call it TokenErrorResponse:
TokenErrorResponse = public class public error: String; error_description: String; error_uri: String; end;
So far so straightforward. No need for any serialized name overrides. The tricky bit comes next, although it’s not really that tricky.
In the onResponse handler for the request, the declared response type (TokenResponse) is automatically represented on the response body property, in the case of a successful request. But for unsuccessful requests we have not told the Retrofit what to expect (if anything).
In that event Retrofit still provides access to any response body it may have received, and we access it in that case via the errorBody property of the response object in our handler.
In the case of this particular client method, we know that the response will be one of those JSON error objects so we can get that in the form of a string:
method LoginActivity.onResponse(request: Call<TokenResponse>; response: Response<TokenResponse>); begin if response.code = 200 then begin // .. end else begin var errorBody := response.errorBody.string; // ... end; end;
In reality you probably wouldn’t simply assume that the response is a valid string and would do some additional checks before trying to work with the error body, but for the purposes of this exercise… Great!
But now what do we do with it ?
Simple: We have declared a POPO to represent that error. All we need to do is transform the JSON into an instance of that POPO, which is precisely what Gson is for. All we need to do is get ourselves a suitable Gson instance and use it to transform the JSON string to an instance of TokenErrorResponse for us:
var errorBody := response.errorBody.string; var gson := (new GsonBuilder).setLenient.create; var errorResponse := gson.fromJson(errorBody, typeOf(TokenErrorResponse)); // Now we can use the POPO to work with the error response var message := "Error: " + errorResponse.error_description;
Similar techniques would obviously also work in the case of requests where even successful requests might return different responses under different circumstances. In those cases you would implement a Retrofit with a method declared as returning a suitably general purpose response body type (e.g. String) and use Gson again as above, or some other object mapping technology as appropriate in the circumstances.