Android Kotlin Gson Slow Json Deserialization - android

I have a large json which i have to deserialize, and i'm only interested of certain parts of if. The Pojos i am using are something like this:
data class Response<T>(
val header: JHeader,
val result: T
)
data class JHeader(
val success: Int,
val error: List<String>
)
class Character{
#SerializedName("id_") val id: Int
#SerializedName("levelA") val level: String
#SerializedName("a3") val unit: String = ""
constructor(id: Int, level: String) {
this.id = id
this.level= level
}
}
Relevant part of the retrofit adapter:
val retrofit = Retrofit.Builder()
.addCallAdapterFactory(rxAdapter)
.addConverterFactory(GsonConverterFactory.create())
.baseUrl(BASE_URL)
.client(httpClient.build())
.build()
And the Impl:
fun getCharacterById(characterID: Int):Observable<Response<List<Character>>> {
return apiService.getCharacter(characterID)
.subscribeOn(Schedulers.io())
}
I'm getting retrofit debug reports of 300ms for a simple call to this service.
My questions are:
When should i consider using a TypeAdapter (i opt for performance over boilerplate, i dont mind writing a few extra lines of code with type adapters). But I don't quite understand what type adapters are for, in what scenarios should i use them.
My Json structure has a lot more attributes than my Character Pojo, i simply realised that using transient / #Expose or keeping it out of the Pojo lead to the same results. Is there any difference between those 3?
As i'm using Kotlin, is there any library/extension that help me deal with this TypeAdapter deserialization stuff?

For me implementing a custom TypeAdapter was a huge performance boost. I use type adapters for every class which needs to be deserialized in huge amounts. In your example that would be (in Java):
public class CharacterTypeAdapter extends TypeAdapter<Character> {
#Override
public void write(final JsonWriter out, final Character character) throws IOException {
out.beginObject();
out.name("id").value(character.getId());
out.name("level").value(character.getLevel());
out.name("unit").value(character.getUnit());
out.endObject();
}
#Override
public Character read(final JsonReader in) throws IOException {
Character character = new Character();
in.beginObject();
while(in.hasNext()){
switch(in.nextName()){
case "id":
character.setId(in.nextInt());
break;
case "level":
character.setId(in.nextString());
break;
case "unit":
character.setId(in.nextString());
break;
}
}
in.endObject();
return character;
}
}
The TypeAdapter must be registered in your Gson configuration as follows:
GsonBuilder gsonBuilder = new GsonBuilder();
gsonBuilder.registerTypeAdapter(Character.class, new CharacterTypeAdapter());
Gson gson = gsonBuilder.create();
The gson instance must be registered with Retrofit:
new Retrofit
.Builder()
.addConverterFactory(GsonConverterFactory.create(gson))
.build()
.createApi(Api.class);
You get blazing fast deserialization.

Related

Is it possible to use kotlinx-datetime with Gson

I have JSON data which is retrieved from the local realm database. I'm trying to convert it into the corresponding data class. I have an ISO date field
{
....
"createdAt" : "2022-05-04T10:16:56.489Z"
....
}
What I'm trying to do is to convert this string date field into kotlinx-datetime's Instant object which is a serializable class. Thus I made my data class as
import kotlinx.datetime.Instant
data class PollPinComment(
...
val createdAt: Instant? = null,
...
)
Which doesn't work and gives an error as follows
com.google.gson.JsonSyntaxException: java.lang.IllegalStateException: Expected BEGIN_OBJECT but was STRING at line 1 column 288 path $[0].createdAt
I'm sure that I might need to write some serialization/deserialization logic with gson to convert this string into Instant object. So my question is how can I do that? Thanks in advance
GSON parses only basic data types, such as Int, String, double...
Other classes that need to be parsed must also consist of these basic data types.
could do like this:
data class PollPinComment(val createdAt: String ){
fun getCreatedAt(): Instant{
return Instant.parse(createdAt)
}
}
You can create a custom Deserializer for this and register it as type adapter to your Gson object.
class InstantDateDeserializer: JsonDeserializer<Instant> {
override fun deserialize(
json: JsonElement?,
typeOfT: Type?,
context: JsonDeserializationContext?
): Instant? {
return json?.asString?.let {
Instant.parse(it)
}
}
}
Create Gson() object pasing that deserializer as type adapter
val gson: Gson = GsonBuilder ()
.registerTypeAdapter(Instant::class.java, DateDeserializer())
.create()
If you are using it with Retrofit
In Retrofit builder pass this to GsonConverterFactory
val retrofit = Retrofit.Builder()
.baseUrl(BASE_URL)
.addConverterFactory(GsonConverterFactory.create(gson))
.build()

Retrofit2 and Gson, deserialize data inside a certain json element

Inside my app i call some remote APIs that give me a response wrapped into a useless "root" json element. I Paste you an example of json response
{
"response": {
"status": {
//here status fields, common to all responses
},
"configuration": {
//here configurations fields
}
}
}
I'm using an Android studio extension for generate kotlin data classes (JsonToKotlinClass) and i obtain four kotlin classes:
-MyResponseClass, Response, Status, Configuration
where MyResponseClass is like
data class MyResponseClass(
val response: Response
)
there's a way to avoid creation of "Response" class by parsing only it's relative json content and get a MyResponseClass look like like
data class MyResponseClass(
val status:Status,
val configuration: Configuration
)
?
From the title of your question I am assuming that you want to automatically parse the json response to the to the simpler MyResponseClass
You can achieve this by using a type adapter for gson. In your case the adapter class will look similar to the following.
class MyResponseClassAdapter : JsonDeserializer<MyResponseClass> {
override fun deserialize(jsonElement: JsonElement, p1: Type?, p2: JsonDeserializationContext?): MyResponseClass {
val content = jsonElement.asJsonObject["response"]
return Gson().fromJson(content , MyResponseClass::class.java)
}
}
Then add this adapter to a gson instance and use that gson instance with your retrofit instance.
val gson = GsonBuilder()
.registerTypeAdapter(MyResponseClass::class.java, MyResponseClassAdapter ())
.create()
Retrofit.Builder()
.addConverterFactory(GsonConverterFactory.create(gson))
.baseUrl("{base_url}")
.client(okHttp)
.build()
Yes, when you get the object, instead of converting it straight away, convert it like this
val jsonObject = JSONObject("data")
val innerObject = jsonObject.getJSONObject("status")
and then convert inner object with gson.
This can of course be minified like this:
val object = Gson().fromJson(JSONObject("response").getJSONObject("status"), MyResponseClass::class.java)

Retrofit2 strange combination of different braces in front of request Body

I am using Retrofit2 and I got stuck on the problem. I wrote simple entity for body:
data class DateRequest(
#JsonAdapter(RetrofitDateSerializer::class)
#SerializedName("date") #Expose val date: OffsetDateTime)
also I wrote custom serializer for it:
class RetrofitDateSerializer : JsonSerializer<OffsetDateTime> {
override fun serialize(
srcDate: OffsetDateTime?,
typeOfSrc: Type?,
context: JsonSerializationContext?
): JsonElement? {
val formatted = DateTimeUtil.convertFromDateTime(srcDate!!)
return JsonPrimitive(formatted)
}}
DateTimeUtil:
fun convertFromDateTime(dateTime: OffsetDateTime): String {
val formatter = formatDateTime()
return formatter.format(dateTime)
}
fun formatDateTime(): DateTimeFormatter {
return DateTimeFormatter.ofPattern("uuuu-MM-dd HH:mm:ss").withLocale(Locale.US)
}
and in request body somehow appears this:
)]}'{"date": "2018-12-07 06:00:00"}
How this ")]}'" could be attached at front of my "date" json in request?
#POST("changecleaning/{userId}/{cleaningId}/{status}")
fun changeCleaning(
#Path("userId") userId: Long,
#Path("cleaningId") cleaningId: Long,
#Path("status") status: Int,
#Body date: DateRequest
): Maybe<Status>
Only that I found is after JsonWriter do some magic in buffer.readByteString() it stores broken body.
GsonRequestBodyConverter:
#Override public RequestBody convert(T value) throws IOException {
Buffer buffer = new Buffer();
Writer writer = new OutputStreamWriter(buffer.outputStream(), UTF_8);
JsonWriter jsonWriter = gson.newJsonWriter(writer);
adapter.write(jsonWriter, value);
jsonWriter.close();
return RequestBody.create(MEDIA_TYPE, buffer.readByteString());
}
I found where was the problem comes from:
Here my custom GsonBuilder:
private fun getGsonFactory(): Gson {
return GsonBuilder()
.setLenient()
.serializeSpecialFloatingPointValues()
.setLongSerializationPolicy(LongSerializationPolicy.DEFAULT)
.generateNonExecutableJson()
.enableComplexMapKeySerialization()
.serializeNulls()
.setPrettyPrinting()
.registerTypeAdapter(CleaningProgress::class.java, CleaningProgressDeserializer())
.create()
}
The line of code below, add that ")]}'" at front of request. Why am I add it to builder? I trust official documentation about this method:
Makes the output JSON non-executable in Javascript by prefixing the generated JSON with some special text. This prevents attacks from third-party sites through script sourcing.
.generateNonExecutableJson()
Now my GsonBuilder looks like this:
private fun getGsonFactory(): Gson {
return GsonBuilder()
.serializeNulls()
.registerTypeAdapter(CleaningProgress::class.java, CleaningProgressDeserializer())
.create()
}

How to send object parameter in Retrofit GET request?

I have a back-end server that works like this
"api/videos?search_object={"cat_id" :2, "channel_id" : 3, etc}
Basily you can give a search object as input and it will filter the list base on that object. Now I want to use this service with Retrofit with something like this
#GET("videos")
Call<VideoListResponse> listVideos(#Query("search_object") VideoSearchObject videoSearchObject);
But the above code doesn't work, I can first convert VideoSearchModel to JSON string that pass it to retrofit like this
#GET("videos")
Call<VideoListResponse> listVideos(#Query("search_object") String jsonString);
I wonder if there is a better more clear way? Any suggestions will be appreciated.
Retrofit 2 supports it. All you have to do is implementing a custom converter factory with the stringConverter() method overridden.
Consider the following Retrofit-friendly interface with a custom annotation:
#Target(PARAMETER)
#Retention(RUNTIME)
#interface ToJson {
}
interface IService {
#GET("api/videos")
Call<Void> get(
#ToJson #Query("X") Map<String, Object> request
);
}
The annotation is used to denote an argument that must be converted to a string.
Mock OkHttpClient to always respond with "HTTP 200 OK" and dump request URLs:
private static final OkHttpClient mockHttpClient = new OkHttpClient.Builder()
.addInterceptor(chain -> {
System.out.println(chain.request().url());
return new Response.Builder()
.request(chain.request())
.protocol(HTTP_1_0)
.code(HTTP_OK)
.body(ResponseBody.create(MediaType.parse("application/json"), "OK"))
.build();
})
.build();
private static final Gson gson = new Gson();
private static final Retrofit retrofit = new Retrofit.Builder()
.client(mockHttpClient)
.baseUrl("http://whatever")
.addConverterFactory(new Converter.Factory() {
#Override
public Converter<?, String> stringConverter(final Type type, final Annotation[] annotations, final Retrofit retrofit) {
if ( !hasToJson(annotations) ) {
return super.stringConverter(type, annotations, retrofit);
}
return value -> gson.toJson(value, type);
}
private boolean hasToJson(final Annotation[] annotations) {
for ( final Annotation annotation : annotations ) {
if ( annotation instanceof ToJson ) {
return true;
}
}
return false;
}
})
.addConverterFactory(GsonConverterFactory.create(gson))
.build();
To test it you can simply invoke the service interface method:
final IService service = retrofit.create(IService.class);
service.get(ImmutableMap.of("k1", "v1", "k2", "v2")).execute();
Result:
http://whatever/api/videos?X={%22k1%22:%22v1%22,%22k2%22:%22v2%22}
Where the X parameter argument is an encoded representation of {"k1":"v1","k2":"v2"}.
You can try the below code, it works for me
#GET("api")
Call<Response> method(#Query("") JSONObject param);
Map<String, String> map = new HashMap<>();
map.put("key", "value");
JSONObject json = new JSONObject(map);
"api/videos?search_object={"cat_id" :2, "channel_id" : 3, etc}
Basically you can give a search object as input
No, you do not give an object as input. You provide multiple parameters enclosed in { } so that it looks like an object (a JavaScript object that is, not a Java object). In reality it is just a string.
The constructed url is just a bunch of characters. There is no such thing as an "object" in an url.
Keep doing it like #Query("search_object") String jsonString. Although you might also want to rename the parameter from jsonString to searchString, since that is what it is. It is not a JSON string. A JSON string would have all " characters escaped like \".

Android Gson ignoring object type adapter in list<object>

class Response<T>(
JHeader header;
T result;
)
class JHeader(
Int success;
List<String> error;
)
class Character{
#SerializedName("id_") Int id;
#SerializedName("levelA") String level
#SerializedName("a3") unit: String = "
Character(Int id, String level) {
this.id = id
this.level= level
}
}
Retrofit.Builder()
.addCallAdapterFactory(rxAdapter)
.addConverterFactory(GsonConverterFactory.create())
.baseUrl(BASE_URL)
.client(httpClient.build())
.build()
public Observable<Response<List<Character>>> getCharacterById(characterID: Int){
return apiService.getCharacter(characterID)
.subscribeOn(Schedulers.io())
}
And i have a custom type adapter made for Character.
GsonBuilder gsonBuilder = new GsonBuilder();
gsonBuilder.registerTypeAdapter(Character.class, new CharacterTypeAdapter());
Gson gson = gsonBuilder.create();
Gson is completly ignoring my type adapter while deserializing the list so i guess because of generic type erasure, the List gets treated like a List Object or something, so my adapter wont be used.
Using Retrofit2. Is there any way i can do his with generics, without having to create custom classes for Lists every time i need to parse a response which contains a list?

Categories

Resources