I'm having a bit of an issue with my custom serializers working with Retrofit, Realm and Gson. Here I am registering type adapters for my three classes:
GsonBuilder builder = new GsonBuilder();
// Register type adapters to modify outgoing json according to server requirements.
try {
builder
.registerTypeAdapter(Class.forName("io.realm.UserProxy"), new UserSerializer())
.registerTypeAdapter(Class.forName("io.realm.FavoriteSongProxy"), new FavoriteSongSerializer())
.registerTypeAdapter(Class.forName("io.realm.ArtistProxy"), new ArtistSerializer())
} catch (ClassNotFoundException e) {
e.printStackTrace();
return null;
}
Gson gson = builder.create();
return new Retrofit.Builder()
.baseUrl("http://10.0.3.2:8080/myUrl/rest/v1/")
.addConverterFactory(GsonConverterFactory.create(gson))
.client(httpClient.build())
.build();
A user has a FavoriteSong, and a FavoriteSong has an Artist. My serializers are set up very similar, here is an example for the FavoriteSongSerializer.
public class FavoriteSongSerializer implements JsonSerializer<FavoriteSong> {
#Override
public JsonElement serialize(FavoriteSong src, Type typeOfSrc, JsonSerializationContext context) {
Log.v("qwer", "serializingFavoriteSong");
final JsonObject jsonObject = new JsonObject();
jsonObject.addProperty("id", src.getId());
jsonObject.add("song", context.serialize(src.getSong()));
return jsonObject;
}
}
Here is the User serializer:
public class UserSerializer implements JsonSerializer<User> {
#Override
public JsonElement serialize(User src, Type typeOfSrc, JsonSerializationContext context) {
Log.v("qwer", "serializingUser");
final JsonObject jsonObject = new JsonObject();
jsonObject.addProperty("id", src.getId());
jsonObject.add("favoriteSong", context.serialize(src.getFavoriteSong()));
return jsonObject;
}
}
The weird part is, both the FavoriteSong and Song serializers are called when I send my request (I can see the log statements), but the UserSerializer is not. Anyone know why this might be happening, or how I might troubleshoot?
/** Edit **/
So as a test I created a TestModel that just contains an id, created a serializer for it, registered a type adapter, and added it to my User model. And it also fails to call the TestModelSerializer (even though it serializes according to the default method).
Try with registerTypeHierarchyAdapter instead of registerTypeAdapter:
.registerTypeHierarchyAdapter(Class.forName("io.realm.UserProxy"), new UserSerializer())
EDIT:
builder
.setExclusionStrategies(new ExclusionStrategy() {
#Override
public boolean shouldSkipField(FieldAttributes f) {
return f.getDeclaringClass().equals(RealmObject.class);
}
#Override
public boolean shouldSkipClass(Class<?> clazz) {
return false;
}
})
.registerTypeAdapter(Class.forName("io.realm.UserProxy"), new UserSerializer())
.registerTypeAdapter(Class.forName("io.realm.FavoriteSongProxy"), new FavoriteSongSerializer())
.registerTypeAdapter(Class.forName("io.realm.ArtistProxy"), new ArtistSerializer())
So if register my User type adapter as:
registerTypeAdapter(User.class, new UserSerializer())
instead of:
.registerTypeAdapter(Class.forName("io.realm.UserProxy"), new UserSerializer())
the User serializer is called. I still have no idea why, but it works.
Related
I'm using retrofit and want to get a simple JSONObject returned. Not an object model... an actual JSONObject object.
I've tried using Call<JSONObject> and Call<ResponseBody> with no luck. Responses are successful but body is empty. I've confirmed the response in postman is NOT empty.
How would I accomplish this?
If your goal is to check the content of the response before parsing, you can use JsonDeserializer along with Gson library. If you don't have Gson implemented yet, add this to your app level gradle:
implementation 'com.squareup.retrofit2:converter-gson:2.4.0'
In your code:
public Retrofit provideRetrofit() {
Gson gson = new GsonBuilder().registerTypeAdapter(ParsedObject.class, new MyDeserializer()).create();
return new Retrofit.Builder()
.addConverterFactory(GsonConverterFactory.create(gson))
.baseUrl(BASE_URL)
.build();
}
ParsedObject.class
public abstract class ParsedObject {
public static final int RESPONSE_TYPE_1 = 1;
public static final int RESPONSE_TYPE_2 = 2;
abstract public int getResponseType();
}
These are for your parsed objects. Since you have 2 types of responses, You can create 2 saperate classes depends on the response type which inherites ParsedObject.class.
TypeOneResponse.class
public class TypeOneResponse extends ParsedObject {
// fields, constructors, getters/setters..
#Overried
public int getResponseType() {
return RESPONSE_TYPE_1;
}
}
you can do the same thing for your TypeTwoResponse.class but returning RESPONSE_TYPE_2 for the getResponseType() method.
MyDeserializer.class
public class CurrentPriceDeserializer implements MyDeserializer {
#Override
public Object deserialize(JsonElement json, Type typeOfT, JsonDeserializationContext context) throws JsonParseException {
// Now, check the Json respons using the raw json (JsonElement) parameter.
// Determine if it is type 1 or 2.
boolean isTypeOne = checkResponseType()
// Deserialize or manually parse objects...
return isTypeOne ? resultTypeOne : resultTypeTwo;
}
}
And finally, in your Retrofit callback
ParsedObject result = deserializedResult;
if (result.getResponseType() == RESPONSE_TYPE_1) {
// cast the object to TypeOneResponse or use it accordingly...
} else {
// case the object to TypeTwoResponse or use it accordingly...
}
I am using Retrofit 2 with GsonConverter. The problem is I have this response:
"responseData": {
"data": "<json array>"
}
As you can see one of the parameters is a JSON array, but it is a string. Should I use TypeAdapter and override the read and write methods? If so can you show how I can do this?
As you can see one of the parameters is a JSON array, but it is a string.
If the response generator is under your control, you should definitely change the response format (both for well-formedness and performance (Gson does not allow to read/write string literals as raw values)).
Should I use TypeAdapter and override the read and write methods?
If you cannot control your server response, you have to implement a custom type adapter with the read method implemented only. To align with that response format, you could define custom mappings like these:
final class Response<T> {
final ResponseData<T> responseData = null;
}
final class ResponseData<T> {
// This is where we're telling Gson to apply the special read strategy, not to all types
#JsonAdapter(RawJsonTypeAdapterFactory.class)
final T data = null;
}
As you can see, you just have to bind a custom type adapter to a specific field only. Despite the JsonAdapter annotation accepts TypeAdapter classes as well, you cannot bind TypeAdapter directly because you need Gson and Type instances.
final class RawJsonTypeAdapterFactory
implements TypeAdapterFactory {
// Gson will instantiate it itself without any issues
private RawJsonTypeAdapterFactory() {
}
#Override
public <T> TypeAdapter<T> create(final Gson gson, final TypeToken<T> typeToken) {
// No additional checks here, we're assuming the necessary fields are properly annotated
final Type type = typeToken.getType();
return new TypeAdapter<T>() {
#Override
public void write(final JsonWriter out, final T value) {
throw new UnsupportedOperationException();
}
#Override
public T read(final JsonReader in)
throws IOException {
return gson.fromJson(in.nextString(), type);
}
}.nullSafe(); // And making the type adapter null-safe
}
}
Now JSON documents like
{
"responseData": {
"data": "[1,2,3]"
}
}
can be easily parsed in plain Java:
private static final Type intArrayResponseType = new TypeToken<Response<int[]>>() {
}.getType();
private static final Gson gson = new Gson();
...
try ( final JsonReader jsonReader = getPackageResourceJsonReader(Q43456942.class, "stringified.json") ) {
final Response<int[]> response = gson.fromJson(jsonReader, intArrayResponseType);
System.out.println(Arrays.toString(response.responseData.data));
}
Output:
[1, 2, 3]
Your Retrofit-bound service might have a method declared this (no type tokens necessary since Retrofit is smart enough to pass the Call parameterization to the underlying Gson converter):
Call<Response<int[]>> getIntArray();
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¶m1=value2¶m3=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
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();
I'm a noob when it comes to basically all forms of storage aside from SharedPreferences and some SQLite. I did some searching and found that JSON+GSON was a fast way to parse Objects and their fields into storable Strings.
So, in my game, I have a Player object which has fields that are also my own classes:
public class Player {
private int something_game_related = 1;
private Skill equipped_skill;
private Item equipped_weapon;
public Player () {}
}
I suspect those classes are the problem, because when I try to run a simple save method:
private class ItemSerializer implements JsonSerializer<Item> {
public JsonElement serialize( Item src, Type typeOfSrc, JsonSerializationContext context ) {
return new JsonPrimitive(src.toString());
}
}
private class SkillSerializer implements JsonSerializer<Skill> {
public JsonElement serialize( Skill src, Type typeOfSrc, JsonSerializationContext context ) {
return new JsonPrimitive(src.toString());
}
}
public void doSave() {
GsonBuilder gson = new GsonBuilder();
//Both custom classes have zero-arg constructors so we don't need to register those
gson.registerTypeAdapter( Item.class, new ItemSerializer() );
gson.registerTypeAdapter( Skill.class, new SkillSerializer() );
Gson g = gson.create();
String mPlayer = "";
Type player = new TypeToken<Player>(){}.getType();
try{
mPlayer = g.toJson( GameView.mPlayer, player );
}
catch (Exception e) {e.printStackTrace();}
}
I get this Exception: java.lang.IllegalStateException: How can the type variable not be present in the class declaration!
My Question is..
How do I get these custom serializers to work? Like I said, I'm a noob.. but it looks like I did it right..
In the docs it says (kind of in the fine print) that static fields are excluded: http://sites.google.com/site/gson/gson-user-guide#TOC-Excluding-Fields-From-Serialization
You can do something like "excludeFieldsWithModifier(Modifier.STATIC)" in the GSON builder to include them.