The remote server returns a response with a body:
{
done: true,
response: {
result: {
// data
}
}
}
In case of an error, a response with 200
status and a body:
{
done: false,
error: {
message: ""
}
}
Before JSON is transferred to typed entities, I want to set the result
data as the root of the response.
{
// data
}
I can't create a wrapper model for all functions of Retrofit's interface (this is a long story why).
I have created a custom factory ProxyGsonConverterFactory
(based on GsonConverterFactory.java). It works as expected.
Look at GsonResponseBodyConverter
class. In order to change the json root, the class uses org.json.JSONObject
. It parses a string into an object and gets resultData
. Then convert the resultData
back to string and use gson
to cast it to generic T
.
class ProxyGsonConverterFactory (
private val gson: Gson
) : Converter.Factory() {
override fun responseBodyConverter(
type: Type, annotations: Array<Annotation>, retrofit: Retrofit
): Converter<ResponseBody, *> {
val adapter = gson.getAdapter(TypeToken.get(type))
return GsonResponseBodyConverter(gson, adapter)
}
override fun requestBodyConverter(
type: Type,
parameterAnnotations: Array<Annotation>,
methodAnnotations: Array<Annotation>,
retrofit: Retrofit
): Converter<*, RequestBody> {
val adapter = gson.getAdapter(TypeToken.get(type))
return GsonRequestBodyConverter(gson, adapter)
}
}
internal class GsonResponseBodyConverter<T>(
private val gson: Gson,
private val adapter: TypeAdapter<T>
) : Converter<ResponseBody, T> {
@Throws(IOException::class)
override fun convert(value: ResponseBody): T {
val jsonObject = JSONObject(value.string())
val jsonPayload = jsonObject.optJSONObject("response")?.opt("result")
if (jsonPayload == null) {
val errorMessage = jsonObject.optJSONObject("error")?.toString()
throw ProxyExecutionException(errorMessage ?: "GsonResponseBodyConverter: payload is null")
}
val modifiedJsonStr = jsonPayload.toString()
val jsonReader = gson.newJsonReader(modifiedJsonStr.reader())
val result = adapter.read(jsonReader)
if (jsonReader.peek() != JsonToken.END_DOCUMENT) {
throw JsonIOException("JSON document was not fully consumed.")
}
jsonReader.close()
return result
}
}
internal class GsonRequestBodyConverter<T>(
private val gson: Gson,
private val adapter: TypeAdapter<T>
) : Converter<T, RequestBody> {
@Throws(IOException::class)
override fun convert(value: T): RequestBody {
val buffer = Buffer()
val writer = OutputStreamWriter(buffer.outputStream(), StandardCharsets.UTF_8)
val jsonWriter = gson.newJsonWriter(writer)
adapter.write(jsonWriter, value)
jsonWriter.close()
return buffer.readByteString().toRequestBody(MEDIA_TYPE)
}
companion object {
private val MEDIA_TYPE: MediaType = "application/json; charset=UTF-8".toMediaType()
}
}
The GsonResponseBodyConverter
parses a JSON string twice. With org.json.JSONObject
it takes more time. Can I solve the task with only one parse?
For example, the function ProxyGsonConverterFactory.responseBodyConverter
contains Type
of generic T to create gson
adapter. Can we put adapter of T to parent adapter ResponseWrapper
in order to parse one time and then return only T body?
data class ResponseWrapper <T>(
val done: boolean
val response: ResponseBody<T>
)
data class ResponseBody <T>(
val result: T
)
The
GsonResponseBodyConverter
parses a JSON string twice. Withorg.json.JSONObject
it takes more time. Can I solve the task with only one parse?
Yes, Gson provides API similar to the org.json.*
one with its JsonElement
class and subclasses. Additionally there is TypeAdapter#fromJsonTree(JsonElement)
so you can directly use the parsed value without having to create an intermediate String again, for example:
override fun convert(value: ResponseBody): T {
val responseJson = gson.fromJson(value.charStream(), JsonObject::class.java)
val resultJson: JsonElement? = responseJson.getAsJsonObject("response")
?.get("result")
if (resultJson != null) {
return adapter.fromJsonTree(resultJson)
}
val errorMessage = responseJson.getAsJsonObject("error")
?.getAsJsonPrimitive("message")?.asString
throw ProxyExecutionException(errorMessage ?: "GsonResponseBodyConverter: payload is null")
}
The disadvantage with this is that it still has to fully parse the response as JsonObject
first before actually deserializing it.
If you know that "done": ...
always comes first, or make assumptions that "response": ...
always means success, you can implement this more efficiently by directly inspecting the JSON from the JsonReader
. The disadvantage is that this becomes more verbose and error-prone:
override fun convert(value: ResponseBody): T {
val jsonReader = gson.newJsonReader(value.charStream())
var result: T? = null
jsonReader.beginObject()
while (jsonReader.hasNext()) {
val name = jsonReader.nextName()
when (name) {
"response" -> {
if (result != null) {
throw ProxyExecutionException("Duplicate result")
}
jsonReader.beginObject()
require(jsonReader.nextName() == "result")
// If you don't want to validate the remainder of the JSON data,
// you can instead directly return the result here
result = adapter.read(jsonReader)
jsonReader.endObject()
}
"error" -> {
// You might want to implement more lenient handling here if
// no message exists
jsonReader.beginObject()
require(jsonReader.nextName() == "message")
val message = jsonReader.nextString()
throw ProxyExecutionException(message)
}
// Ignore other properties
// You might want to handle this differently
else -> jsonReader.skipValue()
}
}
jsonReader.endObject()
if (jsonReader.peek() != JsonToken.END_DOCUMENT) {
throw JsonIOException("JSON document was not fully consumed.")
}
return result ?:
throw ProxyExecutionException("Response contains neither error not success payload")
}
(Note that I haven't tested this yet, and it might need adjustments for error handling or handling of unexpected JSON data, but it should give a rough idea of how to implement it.)