Custom converter to subclass with Moshi - android

I have a User class. And two subclasses. Parent and Child.
I get json from my server with {"user":"..."} and need to convert it to parent or to child depending on user.type
As I understand I need to add custom converter this way:
Moshi moshi = new Moshi.Builder()
.add(new UserAdapter())
.build();
Here's my implementation of UserAdapter. I know it's dummy, but it's not working even this way:
public class UserAdapter {
#FromJson
User fromJson(String userJson) {
Moshi moshi = new Moshi.Builder().build();
try {
JSONObject jsonObject = new JSONObject(userJson);
String accountType = jsonObject.getString("type");
switch (accountType) {
case "Child":
JsonAdapter<Child> childJsonAdapter = moshi.adapter(Child.class);
return childJsonAdapter.fromJson(userJson);
case "Parent":
JsonAdapter<Parent> parentJsonAdapter = moshi.adapter(Parent.class);
return parentJsonAdapter.fromJson(userJson);
}
} catch (JSONException | IOException e) {
e.printStackTrace();
}
return null;
}
#ToJson
String toJson(User user) {
Moshi moshi = new Moshi.Builder().build();
JsonAdapter<User> jsonAdapter = moshi.adapter(User.class);
String toJson = jsonAdapter.toJson(user);
return toJson;
}
First of all I get following exception with this code.
com.squareup.moshi.JsonDataException: Expected a string but was BEGIN_OBJECT at path $.user
And second, I believe there's a better way to do it. Please advice.
Upd. here's stacktrace for the error:
com.squareup.moshi.JsonDataException: Expected a name but was BEGIN_OBJECT at path $.user
at com.squareup.moshi.JsonReader.nextName(JsonReader.java:782)
at com.squareup.moshi.ClassJsonAdapter.fromJson(ClassJsonAdapter.java:141)
at com.squareup.moshi.JsonAdapter$1.fromJson(JsonAdapter.java:68)
at com.squareup.moshi.JsonAdapter.fromJson(JsonAdapter.java:33)
at retrofit.MoshiResponseBodyConverter.convert(MoshiResponseBodyConverter.java:33)
at retrofit.MoshiResponseBodyConverter.convert(MoshiResponseBodyConverter.java:23)
at retrofit.OkHttpCall.parseResponse(OkHttpCall.java:148)
at retrofit.OkHttpCall.execute(OkHttpCall.java:116)
at retrofit.RxJavaCallAdapterFactory$CallOnSubscribe.call(RxJavaCallAdapterFactory.java:111)
at retrofit.RxJavaCallAdapterFactory$CallOnSubscribe.call(RxJavaCallAdapterFactory.java:88)
at rx.Observable$2.call(Observable.java:162)
at rx.Observable$2.call(Observable.java:154)
at rx.Observable$2.call(Observable.java:162)
at rx.Observable$2.call(Observable.java:154)
at rx.Observable.unsafeSubscribe(Observable.java:7710)
at rx.internal.operators.OperatorSubscribeOn$1$1.call(OperatorSubscribeOn.java:62)
at rx.internal.schedulers.ScheduledAction.run(ScheduledAction.java:55)
at java.util.concurrent.Executors$RunnableAdapter.call(Executors.java:422)
at java.util.concurrent.FutureTask.run(FutureTask.java:237)
at java.util.concurrent.ScheduledThreadPoolExecutor$ScheduledFutureTask.access$201(ScheduledThreadPoolExecutor.java:152)
at java.util.concurrent.ScheduledThreadPoolExecutor$ScheduledFutureTask.run(ScheduledThreadPoolExecutor.java:265)
at java.util.concurrent.ThreadPoolExecutor.runWorker(ThreadPoolExecutor.java:1112)
at java.util.concurrent.ThreadPoolExecutor$Worker.run(ThreadPoolExecutor.java:587)
at java.lang.Thread.run(Thread.java:818)

This seems to me like the example you want to follow for your custom de/serialization of your JSON data: https://github.com/square/moshi#another-example
It uses an intermediate class that corresponds to the JSON structure, and Moshi will inflate it automatically for you. Then, you can use the inflated data to build your specialized user classes. For example:
// Intermediate class with JSON structure
class UserJson {
// Common JSON fields
public String type;
public String name;
// Parent JSON fields
public String occupation;
public Long salary;
// Child JSON fields
public String favorite_toy;
public Integer grade;
}
abstract class User {
public String type;
public String name;
}
final class Parent extends User {
public String occupation;
public Long salary;
}
final class Child extends User {
public String favoriteToy;
public Integer grade;
}
Now, the adapter:
class UserAdapter {
// Note that you pass in a `UserJson` object here
#FromJson User fromJson(UserJson userJson) {
switch (userJson.type) {
case "Parent":
final Parent parent = new Parent();
parent.type = userJson.type;
parent.name = userJson.name;
parent.occupation = userJson.occupation;
parent.salary = userJson.salary;
return parent;
case "Child":
final Child child = new Child();
child.type = userJson.type;
child.name = userJson.name;
child.favoriteToy = userJson.favorite_toy;
child.grade = userJson.grade;
return child;
default:
return null;
}
}
// Note that you return a `UserJson` object here.
#ToJson UserJson toJson(User user) {
final UserJson json = new UserJson();
if (user instanceof Parent) {
json.type = "Parent";
json.occupation = ((Parent) user).occupation;
json.salary = ((Parent) user).salary;
} else {
json.type = "Child";
json.favorite_toy = ((Child) user).favoriteToy;
json.grade = ((Child) user).grade;
}
json.name = user.name;
return json;
}
}
I think that this is much cleaner, and allows Moshi to do its thing, which is creating objects from JSON and creating JSON from objects. No mucking around with old-fashioned JSONObject!
To test:
Child child = new Child();
child.type = "Child";
child.name = "Foo";
child.favoriteToy = "java";
child.grade = 2;
Moshi moshi = new Moshi.Builder().add(new UserAdapter()).build();
try {
// Serialize
JsonAdapter<User> adapter = moshi.adapter(User.class);
String json = adapter.toJson(child);
System.out.println(json);
// Output is: {"favorite_toy":"java","grade":2,"name":"Foo","type":"Child"}
// Deserialize
// Note the cast to `Child`, since this adapter returns `User` otherwise.
Child child2 = (Child) adapter.fromJson(json);
System.out.println(child2.name);
// Output is: Foo
} catch (IOException e) {
e.printStackTrace();
}

There's now a much better way to do this, using PolymorphicJsonAdapterFactory. See https://proandroiddev.com/moshi-polymorphic-adapter-is-d25deebbd7c5

You probably tried to implement you parsing according to: https://github.com/square/moshi#custom-type-adapters
There String is used as an argument of #FromJson method, so it can be magically parsed to some mapping helper class or String and we have to parse it manually, right? Actually no, you can either use mapping helper class or Map.
Thus your exception Expected a string but was BEGIN_OBJECT at path $.user was caused by Moshi trying to get that user as a String (because that's what you implied in your adapter), whereas it is just another object.
I don't like parsing ALL possible fields to some helper class as in case of polymorphism that class might become very big and you need to rely or remembering/commenting code.
You can handle it as a map - that is default model for unknown types - and convert it to json, so in your case that would look something like:
#FromJson
User fromJson(Map<String, String> map) {
Moshi moshi = new Moshi.Builder().build();
String userJson = moshi.adapter(Map.class).toJson(map);
try {
JSONObject jsonObject = new JSONObject(userJson);
String accountType = jsonObject.getString("type");
switch (accountType) {
case "Child":
JsonAdapter<Child> childJsonAdapter = moshi.adapter(Child.class);
return childJsonAdapter.fromJson(userJson);
case "Parent":
JsonAdapter<Parent> parentJsonAdapter = moshi.adapter(Parent.class);
return parentJsonAdapter.fromJson(userJson);
}
} catch (JSONException | IOException e) {
e.printStackTrace();
}
return null;
}
Of course you can just handle map directly: retrieve "type" string and then parse the rest of map to chosen class. Then there is no need to use JSONObject at all with nice benefit of not being dependent on Android and easier testing of parsing.
#FromJson
User fromJson(Map<String, String> map) {
Moshi moshi = new Moshi.Builder().build();
try {
String userJson = moshi.adapter(Map.class).toJson(map);
switch (map.get("type")) {
case "Child":
JsonAdapter<Child> childJsonAdapter = moshi.adapter(Child.class);
return childJsonAdapter.fromJson(userJson);
case "Parent":
JsonAdapter<Parent> parentJsonAdapter = moshi.adapter(Parent.class);
return parentJsonAdapter.fromJson(userJson);
}
} catch (IOException e) {
e.printStackTrace();
}
return null;
}

Related

when syncing webservices with SQLite, array and object error appear

i am trying to get my json data into my sqlite, from web services to android.
This is the error that i get when i start syncing.
error stating Expected BEGIN_ARRAY but was BEGIN_OBJECT at line 1 column 2 path $
The error appear on this chunk of codes.
public String processSync(String response) {
String status = FAILED;
try {
String formattedData = getFormattedData(response);
final GsonBuilder gsonBuilder = new GsonBuilder();
final Gson gson = gsonBuilder.create();
ProductDataBean[] productDataBeanArray = gson.fromJson(formattedData, ProductDataBean[].class);
AdministratorHelper administratorHelper = new AdministratorHelper();
status = administratorHelper.insertProductData(context, productDataBeanArray);
} catch (Exception e) {
e.printStackTrace();
}
return status;
}
AdministratorHelper.java
public class AdministratorHelper {
DBLiteUtils dbLiteUtils;
AdministratorDao dao = new AdministratorDao();
public String insertProductData(Context context, ProductDataBean[] productDataBeanArray){
String status = FAILED;
try{
dbLiteUtils = new DBLiteUtils(context);
dbLiteUtils.open();
status = dao.insertProductData(dbLiteUtils, productDataBeanArray);
} catch (Exception e){
e.printStackTrace();
} finally {
dbLiteUtils.close();
}
return status;
}
AdministratorDao.java
public class AdministratorDao {
private Cursor cursor;
//Insert Product Details into SQLite
public String insertProductData(DBLiteUtils dbLiteUtils, ProductDataBean[] productDataBeanArray){
String status = FAILED;
try {
if(productDataBeanArray.length > 0) {
//Delete Existing Data
dbLiteUtils.deleteAll("ProductMaster");
//Base Insert Query for inserting New Data into ProductMaster Table
String query = "INSERT INTO ProductMaster(ItemId, Description, Barcode, TransStatusCode) VALUES ";
//Iterate the productDataBeanArray in order to get productDataBean which contains a single product details
for (ProductDataBean productDataBean : productDataBeanArray) {
//Concatenate the Base Insert Query and the product details
query = query+"('"+productDataBean.getItemId().trim()+"', '"+productDataBean.getDescription().trim().replace("\'", "\'\'")+"', '"+productDataBean.getBarcode().trim()+"', '"+productDataBean.getTransStatusCode().trim()+"'),"; //.trim() removes unwanted empty spaces
}
//Remove the last comma from the iterated complete query
query = query.substring(0, query.length()-1);
//execute the query
dbLiteUtils.executeQuery(query);
//return success
status = SUCCESS;
}
} catch (Exception e){
e.printStackTrace();
}
return status;
}
This is my code in the visual studio to get the json data
public class ProductController : ApiController
{
public IEnumerable<Product> Get()
{
List<Product> pproducts;
using (estocktakeEntities entities = new estocktakeEntities())
{
pproducts = entities.Products.ToList();
//return entities.products.ToList();
}
return pproducts;
}
This is a sample of my json
[{"invtid":"02007997 ","ib_itemcode1":"0 ","transtatuscode":"IN","descr":"Pantene C/C Intensive Care Mask 6 x 150m"},
{"invtid":"1","ib_itemcode1":"1","transtatuscode":"IN","descr":"object1"},{"invtid":"13101336 ","ib_itemcode1":"47400179172 ","transtatuscode":"IN","descr":"Gillette Mach 3 Dispenser 8S (X12) "},
{"invtid":"13101473 ","ib_itemcode1":"47400179349 ","transtatuscode":"IN","descr":"Gillette Mach3 Cart 4S (X12)
Let me know if more information is needed, i will be more than willing to upload them.
thank you sir for your time.
EDIT 1
After trying to debug, i had this error, still not sure is it useful but here is the error and the codes related to it.
{"Message":"An error has occurred.","ExceptionMessage":"Value cannot be null.\r\nParameter name: entity","ExceptionType":"System.ArgumentNullException","StackTrace":" at System.Data.Entity.Utilities.Check.NotNull[T](T value, String parameterName)\r\n at System.Data.Entity.DbSet`1.Add(TEntity entity)\r\n at ProductServiceFinal.Controllers.ProductController.Post(Product productrecord)"}
ProductController.Post
public HttpResponseMessage Post([FromBody] Product productrecord)
{
try
{
using (estocktakeEntities entities = new estocktakeEntities())
{
entities.Products.Add(productrecord);
//entities.Entry(productrecord).State = System.Data.Entity.EntityState.Modified;
entities.SaveChanges();
var message = Request.CreateResponse(HttpStatusCode.Created, productrecord);
message.Headers.Location = new Uri(Request.RequestUri + productrecord.invtid.ToString());
return message;
}
}
catch (Exception ex)
{
return Request.CreateErrorResponse(HttpStatusCode.BadRequest, ex);
}
}
The error log is :com.google.gson.JsonSyntaxException: java.lang.IllegalStateException: Expected BEGIN_OBJECT but was STRING at line 1 column So I guess the problem is JSON body which was web service provided.
public String processSync(String response) {
String status = FAILED;
try {
String formattedData = getFormattedData(response);
final GsonBuilder gsonBuilder = new GsonBuilder();
final Gson gson = gsonBuilder.create();
ProductDataBean[] productDataBeanArray = gson.fromJson(formattedData, ProductDataBean[].class);
AdministratorHelper administratorHelper = new AdministratorHelper();
status = administratorHelper.insertProductData(context, productDataBeanArray);
} catch (Exception e) {
Log.e("tag",formattedData); // log out this JSON
e.printStackTrace();
}
return status;
}
And EDIT 1 told me that the problem may occur in web service. (I guess web service returns a wrong JSON).
My advice is that add some "fault-tolerance" code to Android project.
The root cause is probably in the web service.

ARRAY or OBJECT in Retrofit on Android using TypeAdapter, in a two depth level

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.

#Get arbitrary query parameters in Android Annotations

It seems that I am unable to set arbitrary query parameters to a #Get declaration
My endpoint looks like
http://api.lmiforall.org.uk/api/v1/ashe/estimateHours?soc=2349&coarse=true
There are a non trivial amount of parameters to this query, is there a declaration I can use to indicate this to the #Rest interface?
I tried declaring it as this, but it complains about fields being unused.
#Get("estimateHours")
ASHEFilterInfo GetEstimateHours( int soc, boolean coarse, String filters, String breakdown);
java: #org.androidannotations.annotations.rest.Get annotated method has only url variables in the method parameters
Look at AA cookbook.
Try this (not tested):
#Rest(rootUrl = "http://api.lmiforall.org.uk/api/v1/ashe")
public interface MyService {
#Get("/estimateHours?soc={soc}&coarse={coarse}&breakdown={breakdonw}&filters={filters}")
ASHEFilterInfo GetEstimateHoursFiltered( int soc, boolean coarse, String filters, String breakdown);
#Get("/estimateHours?soc={soc}&coarse={coarse}&breakdown={breakdonw}")
ASHEFilterInfo GetEstimateHours( int soc, boolean coarse, String breakdown);
}
When I needed to create #Get request with many dynamic parameteres, and some of them could be duplicated, I had resolved that problem so:
#Rest(rootUrl = "http://example.com:9080/",
converters = { GsonHttpMessageConverter.class },
interceptors = { ApiInterceptor.class })
public interface ExampleApi {
#Get("content/home/product-type/list?{filters}&domain={domain}") //filters is String like "param1=value1&param1=value2&param3=value3"
ProductTypeListResponse getProductTypeList(int domain, String filters);
}
public class ApiInterceptor implements ClientHttpRequestInterceptor {
private static final String TAG = ApiInterceptor.class.getSimpleName();
#Override
public ClientHttpResponse intercept(final HttpRequest request, byte[] body, ClientHttpRequestExecution execution) throws IOException {
final QueryMultiParamsHttpRequest modifiedRequest = new QueryMultiParamsHttpRequest(request);
return execution.execute(modifiedRequest, body);
}
}
public class QueryMultiParamsHttpRequest implements HttpRequest {
private static final String TAG = QueryParametersBuilder.class.getSimpleName();
private HttpRequest httpRequest;
public QueryMultiParamsHttpRequest(final HttpRequest httpRequest) {
this.httpRequest = httpRequest;
}
#Override
public HttpMethod getMethod() {
return httpRequest.getMethod();
}
#Override
public URI getURI() {
final URI originalURI = httpRequest.getURI();
final String query = originalURI.getQuery() != null ? originalURI.getQuery().replace("%3D", "=").replace("%26", "&") : null;
URI newURI = null;
try {
newURI = new URI(originalURI.getScheme(), originalURI.getUserInfo(), originalURI.getHost(), originalURI.getPort(), originalURI.getPath(),
query, originalURI.getFragment());
} catch (URISyntaxException e) {
Log.e(TAG, "Error while creating URI of QueryMultiParamsHttpRequest", e);
}
return newURI;
}
#Override
public HttpHeaders getHeaders() {
return httpRequest.getHeaders();
}
}
So, I created a wrapper for HttpRequest, that can decode symbols "=" and "&". And this wrapper replaces original HttpRequest in ApiInterceptor. This is a little hacky solution, but it works.
I ran into this same issue and came up with a another solution that while far from ideal, works. The particular problem I was trying to solve was handling "HATEOAS" links.
What I ended up doing was creating a separate class called HATEOASClient to contain endpoint methods that would not escape the HATEOAS links passed in as params. To do that I basically just looked at an auto generated endpoint method and coped/tweaked the body in my implementation.
These methods use the same RestTemplate instance AndroidAnnotations sets up so you still get access to all the general setup you do on the RestTemplate.
For example:
public ResponseEntity<Foo> postFoo(Foo foo) {
HttpHeaders httpHeaders = new HttpHeaders();
httpHeaders.set(RestHeader.AUTH_TOKEN_HEADER, getClient().getHeader(RestHeader.AUTH_TOKEN_HEADER));
httpHeaders.set(RestHeader.ACCEPT_LANGUAGE_HEADER, getClient().getHeader(RestHeader.ACCEPT_LANGUAGE_HEADER));
httpHeaders.setAuthorization(authentication);
HttpEntity<Foo> requestEntity = new HttpEntity<>(null, httpHeaders);
HashMap<String, Object> urlVariables = new HashMap<>();
urlVariables.put("link", foo.getLinks().getFooCreate().getHref());
URI expanded = new UriTemplate(getClient().getRootUrl().
concat(API_VERSION + "{link}")).expand(urlVariables);
final String url;
try {
url = URLDecoder.decode(expanded.toString(), "UTF-8");
} catch (UnsupportedEncodingException e) {
throw new RuntimeException(e);
}
return getClient().getRestTemplate().
exchange(url, HttpMethod.POST, requestEntity, Foo.class, urlVariables);
}
If all parameters is required you can use #Path annotation.
#Rest(rootUrl = "http://api.lmiforall.org.uk/api/v1/ashe")
public interface MyService {
#Get("/estimateHours?soc={soc}&coarse={coarse}&breakdown={breakdown}&filters={filters}")
ASHEFilterInfo GetEstimateHours(#Path int soc, #Path boolean coarse, #Path String breakdown, #Path String filters);
}
If one of the parameters is optional, there isn't yet a solution that can you can easily pass parameters using Android Annotations. But anybody can contribute to better Android Annotations.
if you define the params for each method then you need to provide them in each request. I thought this was sort of over kill too so what I did was just make a generic get/post request in my api client then just manually enter the values, if you don't define the root url I suppose you could use the QueryStringBuilder class and build the uri that way.
#Rest(rootUrl = "https://path/to/api/", converters = { FormHttpMessageConverter.class,
GsonHttpMessageConverter.class, ByteArrayHttpMessageConverter.class })
public interface ApiClient {
#Get("{uri}")
JsonElement apiGet(String uri);
#Post("{uri}")
JsonObject apiPost(String uri,MultiValueMap data);
RestTemplate getRestTemplate();
void setRootUrl(String rootUrl);
void setRestTemplate(RestTemplate restTemplate);
}
Example usage
JsonElement resp = apiClient.apiGet("method/?random_param=1&another_param=test);
It's not as clean but can be dynamic

Android - Simple XML Framework. #Convert interferes with #Attribute - How to solve this?

I was working on capturing the order of elements contained in tag. Here is all the code:
League.java:
#Root
#Convert(value = LeagueConverter.class)
public class League
{
#Attribute
private String name;
#Element(name="headlines", required = false)
private Headlines headlines;
#Element(name="scores", required = false)
private Scores scores;
#Element(name="standings", required = false)
private Standing standings;
#Element(name="statistics", required = false)
private LeagueStatistics statistics;
public List<String> order = new ArrayList<String>();
// get methods for all variables
}
LeagueConverter.java:
public class LeagueConverter implements Converter<League>
{
#Override
public League read(InputNode node) throws Exception
{
League league = new League();
InputNode next = node.getNext();
while( next != null )
{
String tag = next.getName();
if(tag.equalsIgnoreCase("headlines"))
{
league.order.add("headlines");
}
else if(tag.equalsIgnoreCase("scores"))
{
league.order.add("scores");
}
else if(tag.equalsIgnoreCase("statistics"))
{
league.order.add("statistics");
}
else if(tag.equalsIgnoreCase("standings"))
{
league.order.add("standings");
}
next = node.getNext();
}
return league;
}
#Override
public void write(OutputNode arg0, League arg1) throws Exception
{
throw new UnsupportedOperationException("Not supported yet.");
}
}
Exampe of XML:
<android>
<leagues>
<league name ="A">
<Headlines></Headlines>
<Scores></Scores>
...
</league>
<league name ="B">...</league>
</leagues>
</android>
How I'm calling it and expecting it to behave: (Snippet)
Android android = null;
Serializer serial = new Persister(new AnnotationStrategy());
android = serial.read(Android.class, source);
Log.i("Number of leagues found ",tsnAndroid.getLeagueCount() + ""); // prints fine
League nhl = tsnAndroid.getLeagues().get(0); // works fine
// DOES NOT WORK throws NullPointerEx
League nhl2 = tsnAndroid.getLeagueByName("A");
// DOES NOT WORK throws NullPointerEx
for(String s : nhl.getOrder())
{
Log.i("ORDER>>>>>", s);
}
The problem:
android.getLeagueByName() (Works with #Attribute name) suddenly stops working when I have the converter set, so its like the following from League.java, never gets set.
#Attribute
private String name; // not being set
However, when I comment out the converter declaration in League.java - Every league has an attribute called name and android.getLeagueByName() starts working fine...
Does #Convert for League somehow interfere with #Attribute in League?
Even though this question is outrageously old (as is the SimpleXML library), I will give my two cents.
#Convert annotation works only with #Element, but it does not have any effect on #Attribute. I'm not sure if that's a bug or a feature, but there is another way of handling custom serialized objects - called Transform with Matcher, and it works both with Attributes and with Elements. Instead of using the Converters, you define a Transform class that handles serialization and deserialization:
import java.util.UUID;
import org.simpleframework.xml.transform.Transform;
public class UUIDTransform implements Transform<UUID> {
#Override
public UUID read(String value) throws Exception {
return value != null ? UUID.fromString(value) : null;
}
#Override
public String write(UUID value) throws Exception {
return value != null ? value.toString() : null;
}
}
As you can see, it is more straight-forward than implementing the Convert interface!
Create a similar class for all your objects that require custom de/serialization.
Now instantiate a RegistryMatcher object and register there your custom classes with their corresponding Transform classes. This is a thread-safe object that internally uses a cache, so it might be a good idea to keep it as a singleton.
private static final RegistryMatcher REGISTRY_MATCHER = new RegistryMatcher();
static {
try {
REGISTRY_MATCHER.bind(UUID.class, UUIDTransform.class);
// register all your Transform classes here...
} catch (Exception e) {
throw new RuntimeException(e);
}
}
Finally, you can create a Persister class each time before a conversion and pass it the AnnotationStrategy together with your RegistryMatcher instance. In this factory method below, we will also use an indenting formatter:
private static Persister createPersister(int indent) {
return new Persister(new AnnotationStrategy(), REGISTRY_MATCHER, new Format(indent));
}
Now you can make your serialization/deserialization methods:
public static String objectToXml(Object object, int indent) throws MyObjectConversionException {
ByteArrayOutputStream out = new ByteArrayOutputStream();
Persister p = createPersister(indent);
try {
p.write(object, out, "UTF-8");
return out.toString("UTF-8");
} catch (Exception e) {
throw new MyObjectConversionException("Cannot serialize object " + object + " to XML: " + e.getMessage(), e);
}
}
public static <T> T xmlToObject(String xml, final Class<T> clazz) throws MyObjectConversionException {
Persister p = createPersister(0);
try {
return (T) p.read(clazz, xml);
} catch (Exception e) {
throw new MyObjectConversionException(
"Cannot deserialize XML to object of type " + clazz + ": " + e.getMessage(), e);
}
}
The only issue with this approach is when you want to have different formatting for the same object - e.g. once you want the java.util.Date to have just the date component, while later on you also want to have the time component. Then just extend the Date class, calling it DateWithTime, and make a different Transform for it.
#ElementListUnion will capture the order of elements
The #Convert annotation works only on #Element fields. I am struggling against converting #Attribute fields too but with no success for now...

Serializing a SparseArray<T> with GSON

I want to serialize a custom Java object, so I can use SharedPreferences to store it and retreive it in another Activity. I don't need persistant storage, the SharedPreferences, I wipe them when my application is closed. I'm currently using GSON for this, but it doesn't seem to work well with Android's SparseArray type.
My objects:
public class PartProfile {
private int gameId;
// Some more primitives
private SparseArray<Part> installedParts = new SparseArray<Part>();
// ...
}
public class Part {
private String partName;
// More primitives
}
Serialization:
Type genericType = new TypeToken<PartProfile>() {}.getType();
String serializedProfile = Helpers.serializeWithJSON(installedParts, genericType);
preferences.edit().putString("Parts", serializedProfile).commit();
serializeWithJSON():
public static String serializeWithJSON(Object o, Type genericType) {
Gson gson = new Gson();
return gson.toJson(o, genericType);
}
Deserialization:
Type genericType = new TypeToken<PartProfile>() {}.getType();
PartProfile parts = gson.fromJson(preferences.getString("Parts", "PARTS_ERROR"), genericType);
SparseArray<Part> retreivedParts = parts.getInstalledParts();
int key;
for (int i = 0; i < retreivedParts.size(); i++) {
key = retreivedParts.keyAt(i);
// Exception here:
Part part = retreivedParts.get(key);
// ...
}
Exception:
java.lang.ClassCastException: com.google.gson.internal.LinkedTreeMap cannot be cast to com.mypackage.objects.Part
I don't understand why Gson wants to cast a LinkedTreeMap to my object, I never use one in my entire program. I used to have a HashMap<Integer,Part> before I switched to the SparseArray<Part>, and never had issues with that. Are SparseArrays not supported by Gson, or is there an error on my side?
Edit: It seems that the SparseArray gets deserialized correctly, but not the objects inside. Instead of LinkedTreeMaps, these should be of type Part.
Really there is a way to serialize any kind of SparseArray, here is an example code:
public class SparseArrayTypeAdapter<T> extends TypeAdapter<SparseArray<T>> {
private final Gson gson = new Gson();
private final Class<T> classOfT;
private final Type typeOfSparseArrayOfT = new TypeToken<SparseArray<T>>() {}.getType();
private final Type typeOfSparseArrayOfObject = new TypeToken<SparseArray<Object>>() {}.getType();
public SparseArrayTypeAdapter(Class<T> classOfT) {
this.classOfT = classOfT;
}
#Override
public void write(JsonWriter jsonWriter, SparseArray<T> tSparseArray) throws IOException {
if (tSparseArray == null) {
jsonWriter.nullValue();
return;
}
gson.toJson(gson.toJsonTree(tSparseArray, typeOfSparseArrayOfT), jsonWriter);
}
#Override
public SparseArray<T> read(JsonReader jsonReader) throws IOException {
if (jsonReader.peek() == JsonToken.NULL) {
jsonReader.nextNull();
return null;
}
SparseArray<Object> temp = gson.fromJson(jsonReader, typeOfSparseArrayOfObject);
SparseArray<T> result = new SparseArray<T>(temp.size());
int key;
JsonElement tElement;
for (int i = 0; i < temp.size(); i++) {
key = temp.keyAt(i);
tElement = gson.toJsonTree(temp.get(key));
result.put(key, gson.fromJson(tElement, classOfT));
}
return result;
}
}
and to use it you need to register it in your Gson object, like this:
Type sparseArrayType = new TypeToken<SparseArray<MyCustomClass>>() {}.getType();
Gson gson = new GsonBuilder()
.registerTypeAdapter(sparseArrayType, new SparseArrayTypeAdapter<MyCustomClass>(MyCustomClass.class))
.create();
you can find this example in this gist.
P.S.: I know it's not optimized at all, but it's only an example to give an idea on how to achieve what you need.
It seems that the SparseArray gets deserialized correctly, but not the
objects inside. Instead of LinkedTreeMaps, these should be of type
Part.
Your observation is correct, since SparseArray contains Object (not Part), Gson won't have any clue to make Part as your object type. Hence it map your list as its infamous internal type LinkedTreeMap.
To solve it, I think you won't be able to use SparseArray... Or you may try retreivedParts.get(key).toString(), then use gson to parse the object again. But I don't think it's efficient to do that
As pointed out in the other answers SparseArray's internal implementation uses an Object[] to store the values so Gson cannot deserialize it correctly.
This can be solved by creating a custom Gson TypeAdapterFactory:
import java.io.IOException;
import java.lang.reflect.ParameterizedType;
import java.lang.reflect.Type;
import android.util.SparseArray;
import com.google.gson.Gson;
import com.google.gson.TypeAdapter;
import com.google.gson.TypeAdapterFactory;
import com.google.gson.reflect.TypeToken;
import com.google.gson.stream.JsonReader;
import com.google.gson.stream.JsonWriter;
public class SparseArrayTypeAdapterFactory implements TypeAdapterFactory {
public static final SparseArrayTypeAdapterFactory INSTANCE = new SparseArrayTypeAdapterFactory();
private SparseArrayTypeAdapterFactory() { }
#Override
public <T> TypeAdapter<T> create(Gson gson, TypeToken<T> type) {
// This factory only supports (de-)serializing SparseArray
if (type.getRawType() != SparseArray.class) {
return null;
}
// Get the type argument for the element type parameter `<E>`
// Note: Does not support raw SparseArray type (i.e. without type argument)
Type elementType = ((ParameterizedType) type.getType()).getActualTypeArguments()[0];
TypeAdapter<?> elementAdapter = gson.getAdapter(TypeToken.get(elementType));
// This is safe because check at the beginning made sure type is SparseArray
#SuppressWarnings("unchecked")
TypeAdapter<T> adapter = (TypeAdapter<T>) new SparseArrayTypeAdapter<>(elementAdapter);
// call nullSafe() to make adapter automatically handle `null` SparseArrays
return adapter.nullSafe();
}
private static class SparseArrayTypeAdapter<E> extends TypeAdapter<SparseArray<E>> {
private final TypeAdapter<E> elementTypeAdapter;
public SparseArrayTypeAdapter(TypeAdapter<E> elementTypeAdapter) {
this.elementTypeAdapter = elementTypeAdapter;
}
#Override
public void write(JsonWriter out, SparseArray<E> sparseArray) throws IOException {
out.beginObject();
int size = sparseArray.size();
for (int i = 0; i < size; i++) {
out.name(Integer.toString(sparseArray.keyAt(i)));
elementTypeAdapter.write(out, sparseArray.valueAt(i));
}
out.endObject();
}
#Override
public SparseArray<E> read(JsonReader in) throws IOException {
in.beginObject();
SparseArray<E> sparseArray = new SparseArray<>();
while (in.hasNext()) {
int key = Integer.parseInt(in.nextName());
E value = elementTypeAdapter.read(in);
// Use `append(...)` here because SparseArray is serialized in ascending
// key order so `key` will be > previously added key
sparseArray.append(key, value);
}
in.endObject();
return sparseArray;
}
}
}
This factory serializes SparseArrays as JSON objects with the key as JSON property name and the value serialized with the respective adapter as JSON value, e.g.:
new SparseArray<List<String>>().put(5, Arrays.asList("Hello", "World"))
    ↓ JSON
{"5": ["Hello", "World"]}
You then use this TypeAdapterFactory by creating your Gson instance using a GsonBuilder on which you register the TypeAdapterFactory:
Gson gson = new GsonBuilder()
.registerTypeAdapterFactory​(SparseArrayTypeAdapterFactory.INSTANCE)
.create();

Categories

Resources