I have a sealed class like below,
sealed class ApiResult<T>(
val data: T? = null,
val errors: List<Error>? = null
) {
class Success<T>(data: T?) : ApiResult<T>(data)
class Failure<T>(
errors: List<Error>,
data: T? = null
) : ApiResult<T>(data, errors)
}
And this is my Retrofit API interface,
interface Api {
#POST("login")
suspend fun login(#Body loginRequestDto: LoginRequestDto): ApiResult<LoginResponseDto>
}
What I want to achieve is, internally wrap the LoginResponseDto in BaseResponseDto which has the success, error, and data fields. Then put them in the ApiResult class.
data class BaseResponseDto<T>(
val success: Boolean,
val errors: List<Int>,
val data: T?
)
In this case, LoginResponseDto is my data class. So, Retrofit should internally wrap the LoginResponseDto in BaseResponseDto then I will create the ApiResult response in my custom call adapter. How can I tell Retrofit to internally wrap this? I don't want to include the BaseResponseDto in the API interface every time.
After spending the whole day, I could achieve it. I had to create a custom converter. Here it is,
class ApiConverter private constructor(
private val gson: Gson
) : Converter.Factory() {
override fun responseBodyConverter(
type: Type,
annotations: Array<out Annotation>,
retrofit: Retrofit
): Converter<ResponseBody, *> {
val baseResponseType = TypeToken.get(BaseResponseDto::class.java).type
val finalType = TypeToken.getParameterized(baseResponseType, type)
return Converter<ResponseBody, Any> { value ->
val baseResponse: BaseResponseDto<*> =
gson.fromJson(value.charStream(), finalType.type)
baseResponse
}
}
companion object {
fun create(gson: Gson) = ApiConverter(gson)
}
}
Now I don't have to specify the BaseResponseDto every time in the API interface for retrofit.
Great question!
If you want to implement your own solution you would need a custom Retrofit CallAdapter.
My recommendation would be Sandwich an Open source Library which does the exact same thing.
If you want to implement your own soultion here's one way
Modeling Retrofit Responses With Sealed Classes and Coroutines
Related
I'm trying to do something really simple (at least, should be IMO): create a function that receives a string containing some json and turns that into a Gson object. I've created the following function in my class:
class EasyJson(val url: String, private val responseDataClass: Class<*>) {
var text: String
var json: Gson
init {
text = getJsonFromUrl(URL(url)) //another function does this and is working fine
json = stringToJson(text, responseDataClass::class.java) as Gson
}
private fun <T> stringToJson(data: String, model: Class<T>): T {
return Gson().fromJson(data, model)
}
And here is the calling class:
class CallingClass() {
val url="https://api"
init {
test()
}
data class ApiResponse(
val count: Number,
val next: Number?,
val previous: Number?
)
private fun test(){
val json = EasyJson(url, ApiResponse::class.java)
}
}
However, when I do this I get the following exception:
java.lang.UnsupportedOperationException: Attempted to deserialize a java.lang.Class. Forgot to register a type adapter?
How can I use a generic data class as a parameter for Gson here?
The issue here is most likely the following line:
stringToJson(text, responseDataClass::class.java)
This will use the class of the class, which is always Class<Class>. You should just call your stringToJson function with responseDataClass (without ::class.java):
stringToJson(text, responseDataClass)
Additionally it looks like you treat the stringToJson result incorrectly. The result is an instance of T (actually T? because Gson.fromJson can return null). However, you are casting it to Gson.
The correct code would probably look like this:
class EasyJson<T>(val url: String, private val responseDataClass: Class<T>) {
var text: String
var json: T?
init {
text = getJsonFromUrl(URL(url)) //another function does this and is working fine
json = stringToJson(text, responseDataClass)
}
private fun <T> stringToJson(data: String, model: Class<T>): T? {
return Gson().fromJson(data, model)
}
}
(Though unless your code contains more logic in EasyJson, it might make more sense to move this JSON fetching and parsing to a function instead of having a dedicated class for this.)
Hello) Hope you can help me.
Using kotlin (Retrofit2 + moshi) i getting data from "https://api.spacexdata.com/v3/launches" and parsing it.
All is going fine (i getting attributes like: flight_number, mission_name), but some attributes have "null", like "mission_patch" - there are 111 objects. 109 of them have data at "mission_patch", 2 objects dont have it ("mission_patch":null).
My problem: moshi cannot parse correctly attribute which contains null.
if i using:
data class SpaceXProperty(
val flight_number: Int,
val mission_name: String,
val mission_patch: String)
i getting error: "Failure: Required value "mission_patch" missing at $[1]" - OK i changed data class to next:
data class SpaceXProperty(
val flight_number: Int,
val mission_name: String,
val mission_patch: String?)
with this i getting data, but every object have mission_patch=null. This is uncorrect, bc only 2 objects have mission_patch=null, not all.
Help me please. im new at kotlin, what i doing wrong?
My retrofit service:
private const val BASE_URL = "https://api.spacexdata.com/v3/"
private val moshi = Moshi.Builder()
.add(KotlinJsonAdapterFactory())
.build()
private val retrofit = Retrofit.Builder()
.addConverterFactory(MoshiConverterFactory.create(moshi))
//.addConverterFactory(ScalarsConverterFactory.create())
.baseUrl(BASE_URL)
.build()
interface SpaceXApiService {
#GET("launches")
suspend fun getProperties():List<SpaceXProperty>
}
object SpaceXApi{
val retrofitservice :SpaceXApiService by lazy {
retrofit.create(SpaceXApiService::class.java)
}
}
mission_patch is not in the root object like flight_number etc. It's nested inside links. So your model should match. Try this:
data class SpaceXProperty(
val flight_number: Int,
val mission_name: String,
val links: Links) {
data class Links(val mission_patch: String?)
}
i have a data class like:
data class GetDoctorResponse(
val BrickName: String,
val Class: String,
val DoctorAddress
)
Now i want to get response, so i get it like this:
val myResponse = Gson().fromJson("response in json", Array<GetDoctorResponse>::class.java).toList()
my question is how can i create an extension which takes data and class and return me response like above.
i have tried this:
fun getResponse(data: String, it: Class<T>) : List<it> =
Gson().fromJson(data, Array<it>::class.java).toList()
but T is unresolved here, and i want to get response of any data class i passed.
val response = getResponse(data, SomeClass())
You need to Pass type also.
I didn't test it so let me know it worked or not.
inline fun <reified T : Any> getResponse(data: String) : List<T> =
Gson().fromJson(data, Array<T>::class.java).toList()
Thanks every one who helped me.
what i did is create this extension
fun <T> getResponse(data: String, model: Class<Array<T>>): List<T> =
Gson().fromJson(data, model).toList()
and call it as
val myResponse = getResponse(data, Array<GetDoctorResponse>::class.java)
and its working.
I'm using kotlinx.serialization and implementation("com.jakewharton.retrofit:retrofit2-kotlinx-serialization-converter:0.5.0") with Retrofit2
I'm probably missing something obvious, but I'm just not putting 2 and 2 together on this one.
I have a serializable class that has a list of another serializable class:
#Serializable
data class A(
val resultCount: Int,
val results: List<B>
)
#Serializable
data class B(
val x: Int,
val y: Int,
val z: Int
)
And retrofit setup like this:
fun provideRetrofit(): Retrofit {
val contentType = MediaType.get("application/json")
return Retrofit.Builder()
.baseUrl(BaseUrl)
.addConverterFactory(Json.asConverterFactory(contentType))
.build()
}
I have a service:
#GET("search/")
suspend fun GetStuff(): A
When I make the network call GetStuff expecting to get an instance of A, I get this exception:
kotlinx.serialization.SerializationException: Can't locate argument-less serializer for class A. For generic classes, such as lists, please provide serializer explicitly. So I guess it's expecting me to provide a serializer for the List?
I've read I should be able to use a list serializer like B.serializer().list but I'm not sure where to use that in my context, and when I try to put it anywhere I get Unresolved reference: serializer.
I have a sealed class WebSocketMessage which has some subclasses. The WebSocketMessage has a field named type which is used for differentiating between subclasses.
All of the subclasses have their own field named payload which is of different type for each subclass.
Currently I am using Moshi's PolymorphicJsonAdapterFactory so that these classes can be parsed from JSON and encoded to JSON.
This all works, but what I need is to encode the the payload field to stringified JSON instead of JSON object.
Is there any possibility to write a custom adapter class to help me with this problem? Or is there any other solution so that I will not have to do this stringification manually?
I have tried looking into custom adapters but I can't find how I could pass moshi instance to adapter so that I can encode the given field to JSON and then stringify it, nor did I find anything else that could help me.
The WebSocketMessage class with its subclasses:
sealed class WebSocketMessage(
val type: Type
) {
enum class Type(val type: String) {
AUTH("AUTH"),
PING("PING"),
FLOW_INITIALIZATION("FLOW_INITIALIZATION")
}
class Ping : WebSocketMessage(Type.PING)
class InitFlow(payload: InitFlowMessage) : WebSocketMessage(Type.FLOW_INITIALIZATION)
class Auth(payload: Token) : WebSocketMessage(Type.AUTH)
}
The Moshi instance with PolymorphicJsonAdapterFactory:
val moshi = Moshi.Builder().add(
PolymorphicJsonAdapterFactory.of(WebSocketMessage::class.java, "type")
.withSubtype(WebSocketMessage.Ping::class.java, WebSocketMessage.Type.PING.type)
.withSubtype(
WebSocketMessage.InitFlow::class.java,
WebSocketMessage.Type.FLOW_INITIALIZATION.type
)
.withSubtype(WebSocketMessage.Auth::class.java, WebSocketMessage.Type.AUTH.type)
)
// Must be added last
.add(KotlinJsonAdapterFactory())
.build()
How I encode to JSON:
moshi.adapter(WebSocketMessage::class.java).toJson(WebSocketMessage.Auth(fetchToken()))
I currently get the JSON in the next format:
{
"type":"AUTH",
"payload":{
"jwt":"some_token"
}
}
What I would like to get:
{
"type":"AUTH",
"payload":"{\"jwt\":\"some_token\"}"
}
In the second example the payload is a stringified JSON object, which is exactly what I need.
You can create your own custom JsonAdapter:
#Retention(AnnotationRetention.RUNTIME)
#JsonQualifier
annotation class AsString
/////////////////////
class AsStringAdapter<T>(
private val originAdapter: JsonAdapter<T>,
private val stringAdapter: JsonAdapter<String>
) : JsonAdapter<T>() {
companion object {
var FACTORY: JsonAdapter.Factory = object : Factory {
override fun create(
type: Type,
annotations: MutableSet<out Annotation>,
moshi: Moshi
): JsonAdapter<*>? {
val nextAnnotations = Types.nextAnnotations(annotations, AsString::class.java)
return if (nextAnnotations == null || !nextAnnotations.isEmpty())
null else {
AsStringAdapter(
moshi.nextAdapter<Any>(this, type, nextAnnotations),
moshi.nextAdapter<String>(this, String::class.java, Util.NO_ANNOTATIONS)
)
}
}
}
}
override fun toJson(writer: JsonWriter, value: T?) {
val jsonValue = originAdapter.toJsonValue(value)
val jsonStr = JSONObject(jsonValue as Map<*, *>).toString()
stringAdapter.toJson(writer, jsonStr)
}
override fun fromJson(reader: JsonReader): T? {
throw UnsupportedOperationException()
}
}
/////////////////////
class Auth(#AsString val payload: Token)
/////////////////////
.add(AsStringAdapter.FACTORY)
.add(KotlinJsonAdapterFactory())
.build()