How to handle different schema for errors in Retrofit 2? - java

I have an API which returns one of the following schemas:
Success (data found)
{
item_count: 83,
items_per_page: 25,
offset: 25,
items: [
{ ... },
{ ... },
{ ... },
...
]
}
Failure (no data found)
{
success: false,
error: {
code: 200,
message: "Server is busy"
}
}
I want to use Retrofit 2 with GSON to build a wrapper around this API and convert to POJOs, however I'm uncertain how to handle the fact that the API potentially returns two entirely different schemas. For now, I have the following classes:
public class PaginatedResponse<T> {
private int item_count;
private int items_per_page;
private int offset;
private List<T> items;
public PaginatedResponse<T>(int item_count, int items_per_page, int offset, List<T> items) {
this.item_count = item_count;
this.items_per_page = items_per_page;
this.offset = offset;
this.items = items;
}
public List<T> getItems() {
return this.items;
}
}
public class Item {
private int id;
private String name;
// ...
}
Then for my API interface I have:
public interface API {
#GET("items")
Call<PaginatedResponse<Item>> getItems();
}
Then finally to use this I tried to say:
Retrofit retrofit = new Retrofit.Builder()
.baseUrl("http://localhost")
.addConverterFactory(GsonConverterFactory.create())
.build();
API api = retrofit.create(API.class);
api.getItems().enqueue(new retrofit2.Callback<PaginatedResponse<Broadcast>>() {
#Override
public void onResponse(Call<PaginatedResponse<Broadcast>> call, Response<PaginatedResponse<Broadcast>> response) {
Log.d("SUCCESS", response.body().getItems().toString());
}
#Override
public void onFailure(Call<PaginatedResponse<Broadcast>> call, Throwable t) {
Log.d("FAILURE", t.toString());
}
}
So long as no errors are thrown, this seems to work. But when an error is thrown, I get the following in Logcat and my app crashes:
java.lang.NullPointerException: Attempt to invoke virtual method 'java.lang.String java.lang.Object.toString()' on a null object reference
It seems like because the failure JSON lacks an items property, it's setting List<Item> items to null

It seems like because the failure JSON lacks an items property, it's setting List items to null
Yes, It is. You're getting NullPointerException , because you called toString() on a null object. This is an expected behavior.
Solution
As you have different schema for error and success, you need to create a model with both values. Below given a minimal example,
ResponseModel.java
class ResponseModel {
// ERROR
private final Boolean success;
private final Error error;
// SUCCESS
private final int item_count;
// more success values...
ResponseModel(Boolean success, Error error, int item_count) {
this.success = success;
this.error = error;
this.item_count = item_count;
}
public class Error {
private final int code;
private final String message;
private Error(int code, String message) {
this.code = code;
this.message = message;
}
public int getCode() {
return code;
}
public String getMessage() {
return message;
}
}
public Boolean getSuccess() {
return success;
}
public Error getError() {
return error;
}
public int getItem_count() {
return item_count;
}
}
and in onResponse method, you can check if the response is success or not like this
ResponseModel responseModel = response.body();
if (responseModel.getError() == null) {
// success
doSomethingWithSuccess(responseModel.getItem_count())
} else {
// error
doSomethingWithError(responseModel.getError())
}

Related

How to create a polymorphic structure that can work for both success and error responses in Java?

I'm working on a class that will get a list of strings and process them asynchronously using CompletableFutures. Each string is processed by invoking another class that will perform several operations and return a response or throw an exception if there is an error.
I would like to aggregate the responses that I get, whether they have a valid response or an exception and return them as a list to the caller. I would like the caller to be able to expect a list of SomeResponse and be able to interpret them using polymorphism.
However, I'm stuck on determining if this can be done using polymorphism at all, given that the fields for the success and error response are completely different. I have added some pseudo code below on one alternative I have thought of. Basically have SomeResponse be an interface with an isSuccess method. This will allow the caller to know if it's an error or not. However, the caller would still have to cast it to the correct implementation in order to get the value or the error. Is there a better way to approach this? My requirement is being able to return both a success and error response for each given request in the list. If there is an exception, we don't want to abort the entire operation.
public MyProcessorClass {
private final SomeOtherClass someOtherClass;
public List<SomeResponse> process(List<String> requestList) {
return requestList.stream().map(this::procesRequest)
.collectors(Collect.tolist()):
}
private processRequest(String request) {
CompletableFuture completableFuture = CompletableFuture
.supplyAsync(() => {
return new SomeSuccessResponse(someOtherClass.execute(request));
})
.exceptionally(e -> {
return new SomeErrorResponse(e.getCause);
});
return completableFuture.get();
}
}
public interface SomeResponse {
boolean isSuccess();
}
public class SomeSuccessResponse implements SomeResponse {
private final String value;
#Getter
private final boolean success;
public SomeSuccessResponse(String value) {
this.value = value;
this.success = true;
}
}
public class SomeErrorResponse implements SomeResponse {
private final Throwable error;
#Getter
private final boolean success;
public SomeErrorResponse(Throwable error) {
this.error = error;
this.success = false;
}
}
What you want is the visitor pattern https://en.wikipedia.org/wiki/Visitor_pattern
public class Main {
interface IResponse {
void acceptHandler(IResponseHandler handler);
}
static class ResponseA implements IResponse {
#Override
public void acceptHandler(IResponseHandler handler) {
handler.handle(this);
}
}
static class ResponseB implements IResponse {
#Override
public void acceptHandler(IResponseHandler handler) {
handler.handle(this);
}
}
public interface IResponseHandler {
void handle(ResponseA response);
void handle(ResponseB responseB);
}
public static void main(String[] args) {
final IResponseHandler handler = new IResponseHandler() {
#Override
public void handle(ResponseA response) {
System.out.println("Handle ResponseA");
}
#Override
public void handle(ResponseB responseB) {
System.out.println("Handle ResponseB");
}
};
final IResponse someResponse = new ResponseA();
someResponse.acceptHandler(handler);
}
}

Retrofit 2 - RxJava : Unable to invoke no-args constructor for retrofit2.Call<RemoteDataObjectModel>

I am creating an Android app and the objective is to display a list of Pokemon which can be found using the PokeApi at https://pokeapi.co/
I have two instances, one where it works without RxJava2 and one where it doesn't work with RxJava 2. For both instances I use Retrofit 2.
For when it does not work when I include RxJava2 the error that I recieve is
D/thrown: java.lang.RuntimeException: Unable to invoke no-args constructor for retrofit2.Call<za.co.lbnkosi.discoveryassesment.domain.model.RemoteDataObjectModel>. Registering an InstanceCreator with Gson for this type may fix this problem.
At this point I have looked through a lot of Stackoverflow questions similar to this one and most if not all of them mention deserialization which for me has not worked this far.
I would like to know what the problem is or what I am doing wrong and how I can fix this issue. Below I have included the relevant code
public interface PokeApi {
//Ignore
#GET("pokemon")
Call<RemoteDataObjectModel> getPokemonList(#Query("limit") int limit, #Query("offset") int offset);
#GET("pokemon")
Observable<Call<RemoteDataObjectModel>> getPokemonList2(#Query("limit") int limit, #Query("offset") int offset);
}
public class RemoteDataObjectModel {
#SerializedName("results")
private ArrayList<RemoteDataModel> results;
public ArrayList<RemoteDataModel> getResults() {
return results;
}
public void setResults(ArrayList<RemoteDataModel> results) {
this.results = results;
}
}
public class RemoteDataModel {
#SerializedName("number")
private int number;
#SerializedName("name")
private String name;
#SerializedName("url")
private String url;
public int getNumber() {
String[] urlItems = url.split("/");
return Integer.parseInt(urlItems[urlItems.length -1]);
}
public void setNumber(int number) {
this.number = number;
}
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
public String getUrl() {
return url;
}
public void setUrl(String url) {
this.url = url;
}
}
public class RetrofitComponent {
private static RetrofitComponent INSTANCE;
private PokeApi pokeApi;
private RetrofitComponent(){
OkHttpClient okHttpClient = new OkHttpClient().newBuilder()
.connectTimeout(60, TimeUnit.SECONDS)
.readTimeout(60, TimeUnit.SECONDS)
.writeTimeout(60, TimeUnit.SECONDS)
.build();
Retrofit retrofit = new Retrofit.Builder()
.baseUrl("http://pokeapi.co/api/v2/")
.addConverterFactory(GsonConverterFactory.create())
.addCallAdapterFactory(RxJava2CallAdapterFactory.create())
.client(okHttpClient)
.build();
pokeApi = retrofit.create(PokeApi.class);
}
public static RetrofitComponent getInstance() {
if (INSTANCE == null) {
INSTANCE = new RetrofitComponent();
}
return INSTANCE;
}
public Observable<Call<RemoteDataObjectModel>> getPokemonList(int limit, int offest) {
return pokeApi.getPokemonList2(30,0);
}
}
private void getPokemonList(PokeApiDataSource.PokemonListCallback callback) {
RetrofitComponent.getInstance()
.getPokemonList(100,0)
.subscribeOn(Schedulers.io())
.observeOn(AndroidSchedulers.mainThread())
.subscribe(new Observer<Call<RemoteDataObjectModel>>() {
#Override
public void onSubscribe(Disposable d) {
}
#Override
public void onNext(Call<RemoteDataObjectModel> remoteDataObjectModelCall) {
Log.d("","");
remoteDataObjectModelCall.enqueue(new Callback<RemoteDataObjectModel>() {
#Override
public void onResponse(#NotNull Call<RemoteDataObjectModel> call, #NotNull Response<RemoteDataObjectModel> response) {
loading = true;
RemoteDataObjectModel pokeApiObjects = response.body();
_arrayList = Objects.requireNonNull(pokeApiObjects).getResults();
callback.pokemonListSuccess();
}
#Override
public void onFailure(#NotNull Call<RemoteDataObjectModel> call, #NotNull Throwable t) {
loading = true;
Log.e(TAG, " onFailure: " + t.getMessage());
}
});
}
#Override
public void onError(Throwable e) {
Log.d("thrown", e.toString());
}
#Override
public void onComplete() {
}
});
}
I think Call is the class from Retrofit. It provides a callback function to get the response asynchronously. But since you are going to use RxJava, the nature of Rxjava is already asynchronous. You may not need to get the response as Call. Instead, please try this
public interface PokeApi {
// If you need to get the response body + headers ...
#GET("pokemon")
Observable<Response<RemoteDataObjectModel>> getPokemonList2(#Query("limit") int limit, #Query("offset") int offset);
// If you only need body
#GET("pokemon")
Observable<RemoteDataObjectModel> getPokemonList2(#Query("limit") int limit, #Query("offset") int offset);
// Or the better way, the result from API is only return once. So, Single is more suitable in this case
#GET("pokemon")
Single<RemoteDataObjectModel> getPokemonList2(#Query("limit") int limit, #Query("offset") int offset);
}

Retrofit returning null response Android

I'm using Retrofit to make API call, When I handle the response I get the next error (Need to get the data from the API call) -
Attempt to invoke interface method 'java.lang.Object java.util.List.get(int)' on a null object reference
I don't know if I'm doing it right. Anyway here's my code.
Here's the url link: https://data.gov.il/api/
Retrofit call -
#GET("datastore_search?resource_id=2c33523f-87aa-44ec-a736-edbb0a82975e")
Call<Result> getRecords();
Retrofit base call -
private static Retrofit retrofit;
public static final String BASE_URL = "https://data.gov.il/api/action/";
public static Retrofit getRetrofitInstance() {
if (retrofit == null) {
retrofit = new retrofit2.Retrofit.Builder()
.baseUrl(BASE_URL)
.addConverterFactory(GsonConverterFactory.create())
.build();
}
return retrofit;
}
Model class -
public class Result {
#SerializedName("include_total")
#Expose
private Boolean includeTotal;
#SerializedName("resource_id")
#Expose
private String resourceId;
#SerializedName("fields")
#Expose
private List<Field> fields = null;
#SerializedName("records_format")
#Expose
private String recordsFormat;
#SerializedName("records")
#Expose
private List<Record> records = null;
#SerializedName("limit")
#Expose
private Integer limit;
#SerializedName("_links")
#Expose
private Links links;
#SerializedName("total")
#Expose
private Integer total;
public Boolean getIncludeTotal() {
return includeTotal;
}
public void setIncludeTotal(Boolean includeTotal) {
this.includeTotal = includeTotal;
}
public String getResourceId() {
return resourceId;
}
public void setResourceId(String resourceId) {
this.resourceId = resourceId;
}
public List<Field> getFields() {
return fields;
}
public void setFields(List<Field> fields) {
this.fields = fields;
}
public String getRecordsFormat() {
return recordsFormat;
}
public void setRecordsFormat(String recordsFormat) {
this.recordsFormat = recordsFormat;
}
public List<Record> getRecords() {
return records;
}
public void setRecords(List<Record> records) {
this.records = records;
}
...
Main Activity -
RecallService service = RetrofitClientInstance.getRetrofitInstance().create(RecallService.class);
Call<Result> records = service.getRecords();
records.enqueue(new Callback<Result>() {
#Override
public void onResponse(Call<Result> call, Response<Result> response) {
Log.d(TAG, String.valueOf(response.body().getRecords().get(0).getId())); // ERROR
}
#Override
public void onFailure(Call<Result> call, Throwable t) {
Log.e(TAG, t.getMessage());
}
});
The response, which you are getting from the API, doesn't fit the Result POJO.
The response you get from API is like below:
{
"help": "https://data.gov.il/api/3/action/help_show?name=datastore_search",
"success": true,
"result": {...}
}
By using the Result POJO, you are assuming that you get the response as below, which is a json inside the actual response json, and is not what you actually receive. So, just create a POJO which fairly represents the actual response.
{
"include_total": true,
"resource_id": "2c33523f-87aa-44ec-a736-edbb0a82975e",
"fields": [...],
"records_format": "objects",
"records":[...]
}
Try making a class like below (set the annotations yourself):
class Resp{
Result result;
}
Replace the class Result with Resp, like below and other usages:
#GET("datastore_search?resource_id=2c33523f-87aa-44ec-a736-edbb0a82975e")
Call<Resp> getRecords();
Then, finally you can do:
response.body().getResult().getRecords()
The API link you've shared returns the response in the format below:
{"help": "https://data.gov.il/api/3/action/help_show?name=datastore_search", "success": true, "result": {...}}
You are setting the response object to be of type Result which is actually a sub-element within the root element help in the json response. response.body() would include help and the result would be it's sub-element. Since it is not parsed correctly, you're getting a null response.
You will need to include the root element in your model class and update the API call to use that class type as the response type.

java.lang.IllegalStateException in retrofit

I am using retrofit 2.0 Gson data not getting
When i check Postman i am getting response below
{
"name": "Yashodhan Communication",
"zone": "9-Belgaum",
"tsm_name": "Tarun Patil",
"asm_name": "Shivakumar Patil"
}
Error:
java.lang.IllegalStateException: Expected BEGIN_ARRAY but was
BEGIN_OBJECT at line 1 column 2 path $
APIClient Client
public static Retrofit getClient() {
retrofit = new Retrofit.Builder()
.baseUrl("http://vehiclerescue.in/ideadarpan_beta/")
.addCallAdapterFactory(RxJava2CallAdapterFactory.create())
.addConverterFactory(GsonConverterFactory.create())
.build();
return retrofit;
}
This is Pojo class
public class Appuser_Pojoclass {
#SerializedName("name")
#Expose
private String name;
#SerializedName("zone")
#Expose
private String zone;
#SerializedName("tsm_name")
#Expose
private String tsm_name;
#SerializedName("asm_name")
#Expose
private String asm_name;
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
public String getZone() {
return zone;
}
public void setZone(String zone) {
this.zone = zone;
}
public String getTsm_name() {
return tsm_name;
}
public void setTsm_name(String tsm_name) {
this.tsm_name = tsm_name;
}
public String getAsm_name() {
return asm_name;
}
public void setAsm_name(String asm_name) {
this.asm_name = asm_name;
}
}
Activity class
private void fetchAllAppUserdata()
{
progressBar.setVisibility(View.VISIBLE);
Log.d("getauthkeydisplay","**** "+Idea_Urban.getInstance().get_Authkeyvalues());
ideaInterface.get_AppUsers(Idea_Urban.getInstance().get_Authkeyvalues()).enqueue(new Callback<List<Appuser_Pojoclass>>() {
#Override
public void onResponse(Call<List<Appuser_Pojoclass>> call, Response<List<Appuser_Pojoclass>> response) {
if(app_users != null)
{
progressBar.setVisibility(View.INVISIBLE);
app_users.clear();
}
if(response.body()!=null) {
app_users.addAll(response.body());
Log.d("appuserresponce","***** "+response.body());
// app_userDetailsAdapter.notifyDataSetChanged();
}else
{
progressBar.setVisibility(View.INVISIBLE);
}
}
#Override
public void onFailure(Call<List<Appuser_Pojoclass>> call, Throwable t) {
Log.d("failure","**** ");
Log.d("printmetthodsfailure","faiure"+call.toString() + "\n " + t.toString());
progressBar.setVisibility(View.INVISIBLE);
}
});
}
I found some solution as google suggestion but i donot know how to achieve.
You problem is java.lang.IllegalStateException: Expected BEGIN_ARRAY but was BEGIN_OBJECT at line 1 column 2 path $
So you should check you response of your code .
Your JSON response is JSONObject ,so you should use Call<Appuser_Pojoclass> call
Change
Call<List<Appuser_Pojoclass>> call
to
Call<Appuser_Pojoclass> call
The error is in the Call declaration,change
Call<List<Appuser_Pojoclass>>
with
Call<Appuser_Pojoclass>
Your retrofit call response type is <List<Appuser_Pojoclass>> but your API response is not a list of Appuser_Pojoclass, it is simply a single object of type Appuser_Pojoclass.
Change type from <List<Appuser_Pojoclass>> to Appuser_Pojoclass in your retrofit call
You probably defined your get_AppUsers method as something that returns Call<List<Appuser_Pojoclass>> instead of just Call<Appuser_Pojoclass> so it ends up looking for a JSON array, but then your response only contains a single JSON object which causes the error. Change get_AppUsers to return Call<Appuser_Pojoclass> and then change your callback accordingly and that should fix the problem.

Spring websocket #messagemapping de-serialization issue java.lang.ClassCastException: java.util.LinkedHashMap cannot be cast

I am writing a spring websocket application with StompJS on the client side.
On the client side I am intending to send a List of objects and on the server side when it is mapping into java object, it converts itself into a LinkedHashMap
My client side code is
function stomball() {
stompClient.send("/brkr/call", {}, JSON.stringify(listIds));
}
Listids looks like
[{
"path": "/a/b/c.txt",
"id": 12
}, {
"path": "/a/b/c/d.txt",
"id": 13
}]
List Id object looks like
public class ListId {
private String path;
private Long id;
//getters and setters...
}
The Controller looks like this
#MessageMapping("/call" )
#SendTo("/topic/showResult")
public RetObj process(List<ListId> listIds) {
if (!listIds.isEmpty()) {
for(ListId listId: listIds) {
}
}
So I get a java.lang.ClassCastException: java.util.LinkedHashMap cannot be cast to com.blah.ListId
However when I do the same with normal Spring Controller with RestMapping it works fine, Is there anything with springs MessageMapping annotation that maps objects to java differently than the traditional way
I am not sure why is not casting to ListID
I changed it from a List to an Array and it works! Here is what I did
#MessageMapping("/call" )
#SendTo("/topic/showResult")
public RetObj process(ListId[] listIds) {
if (!listIds.isEmpty()) {
for(ListId listId: listIds) {
}
}
Thanks to this question ClassCastException: RestTemplate returning List<LinkedHashMap> instead of List<MymodelClass>
I know this question has already been answered but here's another solution.
To get Jackson to convert your JSON array to list you'll have to wrap it in another object and serialize/deserialize that object.
So you'll have to send following JSON to server
{
list: [
{
"path": "/a/b/c.txt",
"id": 12
}, {
"path": "/a/b/c/d.txt",
"id": 13
}
]
}
List is wrapped into a another object.
Following is the wrapper class
class ServiceRequest {
private List<ListId> list;
public List<ListId> getList() {
if (list == null) {
list = new ArrayList<ListId>();
}
return list;
}
}
and the message method will become
#MessageMapping("/call" )
#SendTo("/topic/showResult")
public RetObj process(ServiceRequest request) {
List<ListId> listIds = request.getList();
if (!listIds.isEmpty()) {
for(ListId listId: listIds) {
}
}
}
Test Code
import java.util.ArrayList;
import java.util.List;
import org.codehaus.jackson.map.ObjectMapper;
public class TestJackson {
public static void main(String[] args) throws Exception {
System.out.println("Started");
String json = "{\"list\":[{\"path\":\"/a/b/c.txt\",\"id\":12},{\"path\":\"/a/b/c/d.txt\",\"id\":13}]}";
ObjectMapper mapper = new ObjectMapper();
ServiceRequest response = mapper.readValue(json.getBytes("UTF-8"), ServiceRequest.class);
for(ListId listId : response.getList()) {
System.out.println(listId.getId() + " : " + listId.getPath());
}
}
public static class ServiceRequest {
private List<ListId> list;
public List<ListId> getList() {
if (list == null) {
list = new ArrayList<ListId>();
}
return list;
}
}
public static class ListId {
private String path;
private String id;
public String getPath() {
return path;
}
public void setPath(String path) {
this.path = path;
}
public String getId() {
return id;
}
public void setId(String id) {
this.id = id;
}
}
}
Test Output
Started
12 : /a/b/c.txt
13 : /a/b/c/d.txt

Categories