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()
}
Related
I'm implementing Twitter OAuth flows as per:
https://developer.twitter.com/en/docs/authentication/guides/log-in-with-twitter
I am getting a response back for the first step (oauth/request_token) which has a 200 code, but the response body is completely empty.
I'm using Retrofit to call the API, and have hooked up an interceptor OkHttpClient to debug the response like so:
val client = OkHttpClient.Builder().also { builder ->
builder.addInterceptor { chain ->
val request = chain.request()
val response = chain.proceed(request)
response
}
}.build()
Then setting up Retrofit like so:
Retrofit.Builder()
.baseUrl(TWITTER_AUTH_BASE_URL)
.addConverterFactory(GsonConverterFactory.create())
.client(client)
.build()
.create(TwitterAuthRetrofit::class.java)
.getRequestToken(
authorizationHeaders
).enqueue(object : Callback<TwitterRequestToken> {
override fun onResponse(call: Call<TwitterRequestToken>, response: Response<TwitterRequestToken>) {
onSuccess(response.body())
}
override fun onFailure(call: Call<TwitterRequestToken>, t: Throwable) {
onFailure()
}
})
When I debug in the interceptor, I can see the response is successful (200) but the response body is empty, which I think is causing my Gson deserialization to fail.
The result of calling response.body.contentLength() in the interceptor is -1.
The result of calling response.code in the interceptor is 200.
Here is the model I am attempting to deserialize the response body to:
data class TwitterRequestToken(
#SerializedName(value = "oauth_token")
val token: String,
#SerializedName(value = "oauth_token_secret")
val tokenSecret: String,
#SerializedName(value = "oauth_callback_confirmed")
val callbackConfirmed: Boolean
)
Note I am using #SerializedName to provide the keys for the response body, whilst the names of my properties are arbitrary to our app (we use camel case). I add a GsonConverterFactory to the Retrofit instance using the builder and have done this in the same way for many other requests before with no issues.
Here is the response I am getting from the API, which I am looking at via debugging in the interceptor above:
Response{protocol=h2, code=200, message=, url=https://api.twitter.com/oauth/request_token}
And here is the cause message from the Throwable I am getting in the onFailure callback from Retrofit:
com.google.gson.stream.MalformedJsonException:
Use JsonReader.setLenient(true) to accept malformed JSON at line 1 column 1 path $
Has anyone got any idea what might cause this?
Finally figured it out, hope this helps someone in future...
The response body from the Twitter API for oauth/request_token isn't encoded as JSON; you will need to read it from the response buffer. Specifically, when implementing the API with Retrofit, you will want your Retrofit interface to return ResponseBody (rather than your custom class), remove GSON from the Retrofit builder and, in the onResponseCallback from Retrofit, write the following code to read the buffer to a string, then split the string on & to get each key val pair, then you can split each of these on = and make sure you have all 3 values before constructing your model:
override fun onResponse(call: Call<ResponseBody>, response: Response<ResponseBody>) {
response.body()?.also { body ->
body.source().readString(Charsets.UTF_8).split('&').map { param ->
param.split('=').let { keyVal ->
keyVal[0] to keyVal[1]
}
}.toMap().let { paramMap ->
val oauthToken = paramMap["oauth_token"]
val oauthTokenSecret = paramMap["oauth_token_secret"]
val oauthCallbackConfirmed = paramMap["oauth_callback_confirmed"]?.toBoolean()
if (oauthToken == null || oauthTokenSecret == null || oauthCallbackConfirmed == null) {
onFailure()
} else {
onSuccess(
TwitterRequestToken(
oauthToken,
oauthTokenSecret,
oauthCallbackConfirmed
)
)
}
}
} ?: onFailure()
}
I'm trying to post some data with retrofit 2 but I'm gettins some problems... and don't find any example like this...
This is the body that I have to send:
{
"birthday": "12-01-1987",
"name": bob,
"activity": {
"activity_preferences": {
"user_subjects": [4,7,8],
"user_allergies": [1,6,10],
}
}
}
This is my data class:
data class GenericFormDataEntity(
var birthday: String,
var name: String,
#SerializedName("activity")
var food: ActivityEntity?
)
data class ActivityEntity(#SerializedName("activity_preferences")val activityPreferences: ActivityPreferencesEntity)
data class ActivityPreferencesEntity(#SerializedName("user_Subjects")var userSubjects:List<Int>?,#SerializedName("user_allergies")var userAllergies: List<Int>?)
This is the method that I'm trying to build the json:
fun getUserFormEntity(): String{
val paramObject = JSONObject()
paramObject.put("birthday", birthday)
paramObject.put("name", name)
paramObject.put("activity", getActivityEntity())
return paramObject.toString()
}
private fun getActivityEntity(): ActivityEntity{
return ActivityEntity(ActivityPreferencesEntity(selectedSubjectList, selecteAllergiesList))
}
And this is the json that is returning me:
{\"birthday\":\"23-12-2019\",\"name\":Bob,"activity\":\"ActivityEntity(activity_preferences=ActivityPreferencesEntity(user_Subjects=[4,7,8], user_allergies=[1,6,10])"}"
My question is, how can I get the correct json that I have to send as a body:
#Headers("Accept: application/json")
#POST("xxxxxxxx")
suspend fun saveUserData(#Body userFormData: String)
You need to stringify getActivityEntity using Gson.
Gson.toJson(getActivityEntity())
Also, from your API I infer that you are using retrofit why not pass along the entire instance of GenericFormDataEntity as the body for your API.
For enabling this you need to follow by adding GsonConverterFactory.create(gson) to your retrofit.
Retrofit.Builder()
.baseUrl(url)
.addConverterFactory(GsonConverterFactory.create(gson))
.callFactory(okHttpClient)
.build()
How to post raw jsonArray string in kotlin using retrofit
im having timeout response on onFailure method
here is sample of string array i want to post
[{"username":"username4"},{"username":"username2"}]
here is my endpoint definition
#Headers("Content-Type: application/json;charset=UTF-8")
#POST("insert/createuser")
fun postuser(#Body logs:String ):Call<ArrRes>
here are my classes
class ArrRes{
#SerializedName("username")
#Expose
var username: String = ""
#SerializedName("message")
#Expose
var message: String = ""
#SerializedName("status")
#Expose
var status: String = ""
}
here is my posting method
var obj = JSONObject();
var arr = JSONArray();
for (i in 0 until 5){
obj.put("username","username${i}");
arr.put(obj);
}
Log.i("app:sync","${arr.toString()}")
mService!!.postuser(arr.toString()).enqueue(
object : Callback<LogResponse> {
override fun onFailure(call: Call<LogResponse>, t: Throwable) {
Log.i("app:retro:service", "onFailure ${t.message}")
}
override fun onResponse(call: Call<LogResponse>, response: Response<LogResponse>) {
if (response.isSuccessful()) {
Log.i("app:retro:service", "onResponse true ${response.body()!!.toString()}")
} else {
Log.i("app:retro:service", "onResponse false ${response.raw().toString()}")
}
}
}
)
here is sample success post using postman
Thanks for helping :)
I solve this issue by adding this dependencies:
implementation 'com.squareup.retrofit2:converter-scalars:$version'
There are multiple existing Retrofit converters for various data formats. You can serialize and deserialize Java objects to JSON or XML or any other data format and vice versa. Within the available converters, you’ll also find a Retrofit Scalars Converter that does the job of parsing any Java primitive to be put within the request body. Conversion applies to both directions: requests and responses.
https://futurestud.io/tutorials/retrofit-2-how-to-send-plain-text-request-body
The JSON data returned from the server can either return an object, or if that object is null, it returns an empty string ("").
My problem is that my DTO expects an object, but it sees a string and crashes.
PersonDTO
data class PersonDto(
#SerializedName("firstName") val first: String,
#SerializedName("lastName") val last: String,
#SerializedName("favorites") val favorites: FavoriteDto,
)
FavoriteDto
class FavoriteDto(
#SerializedName("color") val color: String,
#SerializedName("number") val number: Int
)
Different responses from server
"person" : {
"firstName": "Steve",
"lastName" : "Johnson",
"favorites" : {
"color": "Purple",
"number": 25
}
}
...
"person" : {
"firstName": "Steve",
"lastName" : "Johnson",
"favorites" : ""
}
I've heard that I might need a custom GSON deserializer, but I've never done anything with GSON other than the out of the box stuff - so I was hoping for a nudge in the right direction.
Thanks!
Easiest hack is that you can add extra fields in the class with the same serialised name but with a String data type. Like this -
data class PersonDto(
#SerializedName("firstName") val first: String,
#SerializedName("lastName") val last: String,
#SerializedName("favorites") val favorites: FavoriteDto,
#SerializedName("favorites") val favoritesStr: String,
)
As there is nothing in Gson as "Required" field, you'll just get a null in your deserialized object if something is missing in the JSON. So, if there is an empty string the FavoriteDto object will be null and not null otherwise.
EDIT
I'm adding some Java code that I have written earlier. This might help:
public class PersonDto {
private FavoriteDto favorites;
private String favoritesStr;
public FavoriteDto getResponseDataObject() {
return favorites;
}
public void setResponseDataObject(FavoriteDto favorites) {
this.favorites = favorites;
}
public String getResponseDataString() {
return favoritesStr;
}
public void setResponseDataString(String favoritesStr) {
this.favoritesStr = favoritesStr;
}
Defining the Deserializar:
public static class ArrayObjectDualityDeserializer implements JsonDeserializer<PersonDto> {
public PersonDto deserialize(JsonElement json, Type typeOfT,
JsonDeserializationContext context) throws JsonParseException {
PersonDto response = new PersonDto();
JsonObject object = json.getAsJsonObject();
if(object.get("favorites").isJsonArray()) {
} else if(object.get("favorites").isJsonObject()) {
try {
FavoriteDto dtoObject = gson.fromJson(object.get("favorites"), FavoriteDto.class);
response.setResponseDataObject(dtoObject);
} catch (JsonSyntaxException e) {
DebugLogger.e("Error " + e);
}
} else if (object.get("favorites").isJsonNull()) {
} else {
response.setResponseDataString(object.get("favorites").getAsString());
}
}
}
And:
public static Gson gson = new GsonBuilder()
.registerTypeAdapter(PersonDto.class, new ArrayObjectDualityDeserializer())
.create();
Lastly:
private static Retrofit retrofit = null;
private static OkHttpClient.Builder httpClient = null;
private static OkHttpClient session_client = null;
httpClient = new OkHttpClient.Builder();
httpClient.addInterceptor(new SessionOkHttpInterceptor());
session_client = httpClient.build();
retrofit = new Retrofit.Builder()
.baseUrl(BASE_URL)
.client(session_client)
.addConverterFactory(GsonConverterFactory.create(gson))
.build();
The answer of #Subrato M. is partially correct. In order to work, you should remove #SerializedName("favorites") annotation from both fields in order to work. In this case, Gson won't throw an error when deserialize multiple json fields named favorites because you don't have any field annotated with that name (also don't name fileds the same as expected field name beucase Gson will try to deserialize). Use the custom deserializer to check if is an object or a string.
Nowadays isn't necessary the #Subrato M's hack, you don't get the right json cause you are missing the data keyword before class FavoriteDto and that is why doesn't exists the get and set methods
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.