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?)
}
Related
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
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.)
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 an Android application written in Kotlin, that gets data from an API, for now it's just a local hosted JSON file. When I'm trying to get the data, I receive the error that my list, persons, is not initialized thus persons == null and didn't receive the data. I'm not sure what I did wrong and how to fix this.
The model
data class Person (
#Json(name = "personId")
val personId: Int,
#Json(name = "personName")
val name: String,
#Json(name = "personAge")
val age: Int,
#Json(name = "isFemale")
val isFemale: Boolean,
)
The JSON response
{
"persons": [{
"personId": 1,
"personName": "Bert",
"personAge": 19,
"isFemale": "false",
}
]
}
The ApiClient
class ApiClient {
companion object {
private const val BASE_URL = "http://10.0.2.2:3000/"
fun getClient(): Retrofit {
val moshi = Moshi.Builder()
.add(customDateAdapter)
.build()
return Builder()
.baseUrl(BASE_URL)
.addConverterFactory(MoshiConverterFactory.create(moshi))
.addCallAdapterFactory(RxJava2CallAdapterFactory.create())
.build()
}
}
}
The Retrofit http methods
interface ApiInterface {
#GET("persons")
fun getPersons(): Observable<List<Person>>
}
and finally the call
class PersonActivity: AppCompatActivity(){
private lateinit var jsonAPI: ApiInterface
private lateinit var persons: List<Person>
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_person)
val retrofit = ApiClient.getClient()
jsonAPI = retrofit.create(ApiInterface::class.java)
jsonAPI.getPersons()
.subscribeOn(Schedulers.io())
.unsubscribeOn(Schedulers.computation())
.observeOn(AndroidSchedulers.mainThread())
.subscribe({ persons = it })
}
}
Expected: Data from the JSON file into the persons list instead of NULL.
Your moshi adapter is expecting the person objects directly but they are nested inside the "persons" Json array.
You should add another model as follow
data class Persons (
#Json(name="persons")
val persons : List<Person>
)
Also change the interface to return the new object.
interface ApiInterface {
#GET("persons")
fun getPersons(): Observable<Persons>
}
Will also need to change the subscription to
.subscribe({ persons = it.persons })
I think there could be one of two issues.
If you are using moshi reflection you will have something like these dependencies in gradle:
//Moshi Core
implementation "com.squareup.moshi:moshi:1.8.0"
//Moshi Reflection
implementation "com.squareup.moshi:moshi-kotlin:1.8.0"
In that case you will need to use the KotlinJsonAdapterFactory:
val moshi = Moshi.Builder()
.add(customDateAdapter)
.add(KotlinJsonAdapterFactory())
.build()
If you are using codegen you'll need this:
apply plugin: 'kotlin-kapt'
...
//Moshi Core Artifact
implementation "com.squareup.moshi:moshi:1.8.0"
//Moshi Codegen
kapt "com.squareup.moshi:moshi-kotlin-codegen:1.8.0"
Additionally, every class you want to deserialise to should be annotated with
#JsonClass(generateAdapter = true) (so the Person and Persons class)