When I try to parse the following JSON with Retrofit, I end up with null member objects.
Parsing:
RestAdapter restAdapter = new RestAdapter.Builder()
.setEndpoint(CallerInfo.API_URL)
.setLogLevel(RestAdapter.LogLevel.FULL)
.build();
InGameInfo igi = restAdapter.create(InGameInfo.class);
Game game = igi.fetchInGameInfo("EUW", "sasquatching");
Log.d("Cancantest", "Game " + game); //Not null
Log.d("Cancantest", "Team one " + game.getTeamOne()); //Null
Game Class:
#SerializedName("teamTwo")
#Expose private Team teamTwo;
#SerializedName("teamOne")
#Expose private Team teamOne;
public void setTeamOne(Team teamOne) {
this.teamOne = teamOne;
}
public void setTeamTwo(Team teamTwo) {
this.teamTwo = teamTwo;
}
public Team getTeamOne() {
return teamOne;
}
public Team getTeamTwo() {
return teamTwo;
}
Team Class:
#SerializedName("array")
#Expose private TeamMember[] teamMembers;
public void setTeamMembers(TeamMember[] teamMembers) {
this.teamMembers = teamMembers;
}
public TeamMember[] getTeamMembers() {
return teamMembers;
}
Example JSON:
{
"game":{
"teamTwo":{
"array":[]
},
"teamOne":{
"array":[]
}
}
}
The JSON contains a top level "game" entry so you cannot directly deserialize an instance of game. You need another type which has a field of type Game that represents the response.
public class Response {
public final Game game;
public Response(Game game) {
this.game = game;
}
}
You can put your JSON in a string and use Gson directly to test how the response will be deserialized. This behavior has almost nothing to do with Retrofit and all to do with the behavior of Gson.
String data = "...";
Game game = gson.fromJson(data, Game.class);
Response response = gson.fromJson(data, Response.class);
There can be one more reason for somewhat similar behavior: in this case debugger actually has no field members for the response returned from Retrofit.
And the reason for that is proguard. If you are using minifyEnabled true, make sure you explicitly tell it to keep your POJOs. It can be something like that:
#save model classes
-keep class com.example.app.**.model.** {*; }
Related
I have been trying out how to work with RxJava2 and Retrofit 2 to use Github API. Trying to access the link :
<https://api.github.com/search/repositories?q=topic:ruby+topic:rails>.I want to display the NAME and NODE_ID. But Have been stuck with the error:
java.lang.IllegalStateException: Expected BEGIN_ARRAY but was BEGIN_OBJECT at line 1 column 2 path $ .
Here is the Client class for Retrofit.
public class Client {
public static final String GITHUB_BASE_URL = "https://api.github.com/search/";
private static Client instance;
private static GetData data;
public Client() {
final Gson gson = new GsonBuilder().setFieldNamingPolicy(FieldNamingPolicy.
LOWER_CASE_WITH_UNDERSCORES).create();
final Retrofit retrofit = new Retrofit.Builder().baseUrl(GITHUB_BASE_URL).
addCallAdapterFactory(RxJavaCallAdapterFactory.create())
.addConverterFactory(GsonConverterFactory.create(gson)).build();
data = retrofit.create(GetData.class);
}
public static Client getInstance(){
if(instance==null)
instance = new Client();
return instance;
}
public Observable<List<itemsClass>> getAllUsers(){
return data.getAllUsers();
}
The interface
#GET("repositories?q=topic:ruby+topic:rails")
Observable<List<itemsClass>> getAllUsers();
The data I need to get through the call are like this:
{
"total_count": 2997,
"incomplete_results": false,
"items": [
{
"id": 8514,
"name": "rails",
"owner": {
"node_id": "MDEyOk9yZ2FuaXphdGlvbjQyMjM="
},
}],
},......
And also the MainActivity from where I will display the data.
subscription = Client.getInstance().getAllUsers().
subscribeOn(Schedulers.io()).observeOn(AndroidSchedulers.
mainThread()).subscribe(new Observer<List<ParentClass>>() {
#Override
public void onCompleted() {
Log.d(TAG,"In completed()");
}
#Override
public void onError(Throwable e) {
Log.d(TAG,"In onError()");
Log.d(TAG,""+e);
}
#Override
public void onNext(List<ParentClass> parentClasses) {
Log.d(TAG,"In onNext()");
loadData(parentClasses);
}
});
The issue has nothing to do with RxJava. It's problem with your data model:
Observable<List<itemsClass>> - you are expecting list of entities to come, but in fact your json is entity that has fields total_count, incomplete_results and actual list field items of (apparently) itemsClass. Please use correct model under Observable<...> getAllUsers(); interface method.
The response you get isn't an array.
When using json converters (in your case it's gson), the response must match exactly to the POJO.
You can wrap itemClass with another class like responseClass which will have:
class responseClass {
int total_count;
boolean incomplete_results;
List<itemClass> items;
}
And then, in Client interface:
#GET("repositories?q=topic:ruby+topic:rails")
Observable<responseClass> getAllUsers();
Another option is not to use the auto converter, instead you may do:
#GET("repositories?q=topic:ruby+topic:rails")
Observable<JsonObject> getAllUsers(); //com.google.gson.JsonObject
and then convert only the items array with gson.
Type typeToken = new TypeToken<List<itemClass>>() {}.getType();
List<itemClass> items = new Gson().fromJson(response.get("items"), typeToken);
I am using spring data rest, I have following entities exposed via spring data rest
DonationRequest
#Data
#Entity
#Table(name="donation_request",schema="public")
public class DonationRequest {
#Id
#GeneratedValue(strategy = GenerationType.IDENTITY)
#Column(name="donation_request_id")
Integer donationRequestId;
#Column(name="expiry_datetime")
Date expiryDatetime;
#Column(name="blood_group")
String bloodGroup;
#Column(name="no_of_bottles")
String noOfBottles;
#OneToOne
#JoinColumn(name="hospital_id")
Hospital hospital;
#OneToOne
#JoinColumn(name="user_data_id")
UserData requester;
#Column(name="active")
Boolean active;
}
Hospital
#Data
#Entity
#Table(name="hospital",schema="public")
public class Hospital {
#Id
#GeneratedValue(strategy = GenerationType.IDENTITY)
#Column(name="hospital_id")
Integer hospitalId;
#Column(name="name")
String name;
#Column(name="address")
String address;
#Column(name="loc",columnDefinition = "geometry")
Point loc;
}
Now I have an android client which has the same class definitions as stated above. Hospitals are cached at startup on android client. Now I want to create a donationRequest entity on server. I can do that easily by posting json of donationRequest object to /api/donationRequests. this json contains hospital object also. But the newly created donationRequest and hospital are not linked together.
Following type of json in postman does not create link:
{
"bloodGroup":"AB+",
"hospital":{
"hospitalId":1
}
}
I know that following json does create link:
{
"bloodGroup":"AB+",
"hospital":"/api/hospitals/1"
}
My question is how can I create link using first type of json as that is the natural way to serialize dontaionRequest object from android client? Also I want hospitals to be exposed via /api/hospitals, so removing that rest resource is not an option.
It can be achieved by using a custom HttpMessageConverter and defining a custom content-type which can be anything other than standard (I used application/mjson):
MHttpMessageConverter.java
public class MHttpMessageConverter implements HttpMessageConverter<Object>{
#Override
public boolean canRead(Class<?> aClass, MediaType mediaType) {
if (mediaType.getType().equalsIgnoreCase("application")
&& mediaType.getSubtype().equalsIgnoreCase("mjson"))
return true;
else
return false;
}
#Override
public boolean canWrite(Class<?> aClass, MediaType mediaType) {
return false;
}
#Override
public List<MediaType> getSupportedMediaTypes() {
return new ArrayList<>(Arrays.asList(MediaType.APPLICATION_JSON));
}
#Override
public Object read(Class<?> aClass, HttpInputMessage httpInputMessage) throws IOException, HttpMessageNotReadableException {
ObjectMapper mapper = new ObjectMapper();
Object obj = mapper.readValue(httpInputMessage.getBody(),aClass);
return obj;
}
#Override
public void write(Object o, MediaType mediaType, HttpOutputMessage httpOutputMessage) throws IOException, HttpMessageNotWritableException {
}
}
CustomRestConfiguration.java
#Configuration
public class CustomRestConfiguration extends RepositoryRestConfigurerAdapter {
#Override
public void configureHttpMessageConverters(List<HttpMessageConverter<?>> messageConverters) {
messageConverters.add(new MHttpMessageConverter());
}
}
Spring Data REST is using HATEOAS. To refer to associated resources we have to use links to them:
Create a hospital first
POST /api/hospitals
{
//...
}
response
{
//...
"_links": [
"hostpital": "http://localhost/api/hospitals/1",
//...
]
}
Then get 'hospital' (or 'self') link and add it to the 'donationRequests' payload
POST /api/donationRequests
{
"bloodGroup":"AB+",
"hospital": "http://localhost/api/hospitals/1"
}
Another approach - create first 'donationRequests' without hospital
POST /api/donationRequests
{
//...
}
response
{
//...
"_links": [
"hostpital": "http://localhost/api/donationRequests/1/hospital"
//...
]
}
then PUT hospital to donationRequests/1/hospital using text link to hospital in your payload (pay attention to Content-Type: text/uri-list)
PUT http://localhost/api/donationRequests/1/hospital (Content-Type: text/uri-list)
http://localhost/api/hospitals/1
Info: Repository resources - The association resource
UPDATE
If it's necessary to deal without links to resources we have to make a custom rest controller.
I'm struggling with TypeAdapter. Indeed for a json field, I can have an Array (when it's empty) or an Object (when it's not empty). This can't be changed.
Here is the JSON received :
{
"notifications": [
{
...
}
],
"meta": {
"pagination": {
"total": 13,
"count": 13,
"per_page": 20,
"current_page": 1,
"total_pages": 1,
"links": []
}
}
}
The field concerned is links, as you can see the field is inside pagination, which is inside meta. And that's my issue, I don't know how the TypeAdapter has to handle links in a two depth level.
I used this reply to start building a solution. Here it is :
My Custom TypeAdapter class :
public class PaginationTypeAdapter extends TypeAdapter<Pagination> {
private Gson gson = new Gson();
#Override
public void write(JsonWriter out, Pagination pagination) throws IOException {
gson.toJson(pagination, Links.class, out);
}
#Override
public Pagination read(JsonReader jsonReader) throws IOException {
Pagination pagination;
jsonReader.beginObject();
if (jsonReader.peek() == JsonToken.BEGIN_ARRAY) {
pagination = new Pagination((Links[]) gson.fromJson(jsonReader, Links[].class));
} else if(jsonReader.peek() == JsonToken.BEGIN_OBJECT) {
pagination = new Pagination((Links) gson.fromJson(jsonReader, Links.class));
} else {
throw new JsonParseException("Unexpected token " + jsonReader.peek());
}
return pagination;
}
}
My Pagination class :
public class Pagination {
private int total;
private int count;
#SerializedName("per_page")
private int perPage;
#SerializedName("current_page")
private int currentPage;
#SerializedName("total_pages")
private int totalPages;
private Links links;
Pagination(Links ... links) {
List<Links> linksList = Arrays.asList(links);
this.links = linksList.get(0);
}
}
And I'm building my Gson object like that :
Gson gson = new GsonBuilder().registerTypeAdapter(Pagination.class, new PaginationTypeAdapter()).create();
For now my error is : com.google.gson.JsonParseException: Unexpected token NAME
So I know I'm not doing it right, because I'm building my Gson with pagination. But I don't know how it should be handle. Using a TypeAdapter with meta ?
Any help will be welcome, thanks !
When you implement a custom type adapter, make sure that your type adapter has balanced token reading and writing: if you open a composite token pair like [ and ], you have to close it (applies for both JsonWriter and JsonReader). You just don't need this line to fix your issue:
jsonReader.beginObject();
because it moves the JsonReader instance to the next token, so the next token after BEGIN_OBJECT is either NAME or END_OBJECT (the former in your case sure).
Alternative option #1
I would suggest also not to use ad-hoc Gson object instatiation -- this won't share the configuration between Gson instances (say, your "global" Gson has a lot of custom adapters registered, but this internal does not have any thus your (de)serialization results might be very unexpected). In order to overcome this, just use TypeAdapterFactory that is more context-aware than a "free" Gson instance.
final class PaginationTypeAdapterFactory
implements TypeAdapterFactory {
private static final TypeAdapterFactory paginationTypeAdapterFactory = new PaginationTypeAdapterFactory();
private PaginationTypeAdapterFactory() {
}
static TypeAdapterFactory getPaginationTypeAdapterFactory() {
return paginationTypeAdapterFactory;
}
#Override
public <T> TypeAdapter<T> create(final Gson gson, final TypeToken<T> typeToken) {
// Classes can be compared using == and !=
if ( typeToken.getRawType() != Pagination.class ) {
// Not Pagination? Let Gson pick up the next best-match
return null;
}
// Here we get the references for two types adapters:
// - this is what Gson.fromJson does under the hood
// - we save some time for the further (de)serialization
// - you classes should not ask more than they require
final TypeAdapter<Links> linksTypeAdapter = gson.getAdapter(Links.class);
final TypeAdapter<Links[]> linksArrayTypeAdapter = gson.getAdapter(Links[].class);
#SuppressWarnings("unchecked")
final TypeAdapter<T> typeAdapter = (TypeAdapter<T>) new PaginationTypeAdapter(linksTypeAdapter, linksArrayTypeAdapter);
return typeAdapter;
}
private static final class PaginationTypeAdapter
extends TypeAdapter<Pagination> {
private final TypeAdapter<Links> linksTypeAdapter;
private final TypeAdapter<Links[]> linksArrayTypeAdapter;
private PaginationTypeAdapter(final TypeAdapter<Links> linksTypeAdapter, final TypeAdapter<Links[]> linksArrayTypeAdapter) {
this.linksTypeAdapter = linksTypeAdapter;
this.linksArrayTypeAdapter = linksArrayTypeAdapter;
}
#Override
public void write(final JsonWriter out, final Pagination pagination)
throws IOException {
linksTypeAdapter.write(out, pagination.links);
}
#Override
public Pagination read(final JsonReader in)
throws IOException {
final JsonToken token = in.peek();
// Switches are somewhat better: you can let your IDE or static analyzer to check if you covered ALL the cases
switch ( token ) {
case BEGIN_ARRAY:
return new Pagination(linksArrayTypeAdapter.read(in));
case BEGIN_OBJECT:
return new Pagination(linksTypeAdapter.read(in));
case END_ARRAY:
case END_OBJECT:
case NAME:
case STRING:
case NUMBER:
case BOOLEAN:
case NULL:
case END_DOCUMENT:
// MalformedJsonException, not sure, might be better, because it's an IOException and the read method throws IOException
throw new MalformedJsonException("Unexpected token: " + token + " at " + in);
default:
// Maybe some day Gson adds something more here... Let be prepared
throw new AssertionError(token);
}
}
}
}
Alternative option #2
You can annotate your private Links links; with #JsonAdapter and bind a type adapter factory directly to links: Gson will "inject" links objects directly to Pagination instances, so you don't even need a constructor there.
I am using retrofit for android has been working great but I was wondering if there is a better solution to my problem.
Background
So I have a backend server which may respond with a server related error message in JSON form (not network) like "could not find ID X" etc.
It looks like this
{
"data": {
"errors": {
"base": [
"You could do the following because of blah blah"
]
}
}
}
So the problem is the JSON error object which contains the array "base" might NOT be called "base" it could be called something else completely different. My backend has many kind of error response message name which cannot be changed so easily.
My question, it is possible to deserializise this JSON array without having to know its name in advance.
So far I have been doing this which is becoming a pain.
public class MyRetrofitError
{
#SerializedName(JsonConstants.ERROR_KEY)
private Errors errors;
public Errors getErrors() {
return errors;
}
public void setErrors(Errors errors) {
this.errors = errors;
}
public static class Errors
{
private String errorMessage;
private ErrorCodes errorCodes;
public enum ErrorCodes{
BOOKED_OVER_20,PROMO_CODE_FAILED;
}
#SerializedName("booked_over")
private ArrayList<String> bookedOver= new ArrayList<String>();
#SerializedName("promo_fail")
private ArrayList<String> promoFailed= new ArrayList<String>();
public String getErrorMessage() {
if(promoFailed!= null && promoFailed.size() > 0) {setErrorMessage(promoFailed().get(0)); errorCodes = ErrorCodes.PROMO_CODE_FAILED;}
if(bookedOver!= null && bookedOver.size() > 0){setErrorMessage(bookedOver().get(0)); errorCodes = ErrorCodes.BOOKED_OVER_20; }
return errorMessage;
}
private void setErrorMessage(String errorMessage) {
this.errorMessage = errorMessage;
}
public ErrorCodes getErrorCodes() {
return errorCodes;
}
}
}
Please note: the root "data" object is not present in this, its handled else where via generics so I omitted it for this question
And then I would use the retrofit Error class getBodyAs method to map the incoming error to this model.
Is their a better way? Please note the backend is beyond my control.
Assuming you are using Gson with Retrofit (it is default). You can write a Custom Gson Deserializer for your errors class and save however you would like.
Here is a quick example to give you the idea (not tested)
// Possible error class
public static class Errors {
Map<String, List<String>> errors;
public void addErrors(String key, List<String> issues) {
errors.put(key, issues);
}
}
class ErrorDeserialzer implements JsonDeserializer<Errors> {
#Override
public Errors deserialize(JsonElement json, Type typeOfT, JsonDeserializationContext context)
throws JsonParseException {
Set<Map.Entry<String, JsonElement>> entries = jsonElement.getAsJsonObject().entrySet();
for (Map.Entry<String, JsonElement> issues : entries) {
List<String> values = new ArrayList<>();
for (JsonElement value : issues.getValue().getAsJsonArray()) {
values.add(value.getAsString());
}
errors.add(issues.getKey(), values);
}
return errors;
}
}
EDIT
I also just found this link which may be of help
I have a simple server rest endpoint running Spring -
#RestController
#RequestMapping("/services")
#Transactional
public class CustomerSignInService {
#Autowired
private CustomerDAO customerDao;
#RequestMapping("/customer/signin")
public Customer customerSignIn(#RequestParam(value = "customer") Customer customer) {
//Some Code Here...
return customer;
}
}
I'm trying to pass a Customer object from my Xamarin Android App using this method -
public JsonValue send(String url, SmartJsonSerializer obj)
{
HttpWebRequest request = (HttpWebRequest)HttpWebRequest.Create(new Uri(url));
request.ContentType = "application/json";
request.Method = "POST";
using (var streamWriter = new StreamWriter(request.GetRequestStream()))
{
streamWriter.Write(obj.toJsonString());
}
using (WebResponse response = request.GetResponse())
{
using (Stream stream = response.GetResponseStream())
{
return JsonObject.Load(stream);
}
}
}
But i keep getting Bad Request Exception (Http Error 400) and obviously my code at the server side is not triggered.
SmartJsonSerializer uses JSON.NET to serialize the Customer object to string -
using System;
using Newtonsoft.Json;
namespace Shared
{
public class SmartJsonSerializer
{
public string toJson()
{
return JsonConvert.SerializeObject(this);
}
}
}
Any help appreciated,
thnx!
Typically if you are posting a complex object to an api like this, you would write it in the request body. You do appear to be doing this on the android side.
I am not familiar with Spring, but it looks that you are expecting customer as a url parameter - Try replacing #RequestParam with #RequestBody.
I was struggling with it for a while, but apparently the solution may be find in the server side.
If it helps, You can look at this
#RestController
#RequestMapping("/services")
#Transactional
public class SomeService {
#RequestMapping(value = "/user/signin", method = RequestMethod.POST)
#ResponseBody
public AppUser signIn(#RequestBody AppUser appUser) {
appUser.invoke();
return appUser;
}
}