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.)
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 am working on small project on a Jetpack Compose.
I am trying to data from a static JSON File from this url using Retrofit.
https://firebasestorage.googleapis.com/v0/b/culoader.appspot.com/o/json%2Fcudata.json?alt=media&token=d0679703-2f6c-440f-af03-d4d61305cc84
Network Module
#Module
#InstallIn(SingletonComponent::class)
object NetworkModule {
#Provides
#Singleton
fun proveidesCurrencyService() : CurrencyService{
return Retrofit.Builder()
.baseUrl("https://firebasestorage.googleapis.com/")
.addConverterFactory(GsonConverterFactory.create())
.build()
.create(CurrencyService::class.java)
}
}
Service Class
interface CurrencyService {
//#Streaming
#GET
suspend fun getCurrencyFile(#Url fileUrl:String): Response<ResponseBody>
}
data class CurrencyAPIResponse(
#SerializedName("data") var data: MutableState<List<CurrencyRates>>
)
data class CurrencyRates (
#SerializedName("code") var code : String,
#SerializedName("value") var value : String
)
ViewModel
#HiltViewModel
class CurrencyDataViewModel
#Inject
constructor(private val currencyService: CurrencyService
) : ViewModel()
{
init {
getCurrencyFileData()
}
}
fun getCurrencyFileData() = viewModelScope.launch(Dispatchers.IO) {
val url: String =
"https://firebasestorage.googleapis.com/v0/b/culoader.appspot.com/o/json%2Fcudata.json?alt=media&token=d0679703-2f6c-440f-af03-d4d61305cc84"
val responseBody = currencyService.getCurrencyFile(url).body()
if (responseBody != null) {
Log.d("\nresponsebody", responseBody.string())
val gson = GsonBuilder().setPrettyPrinting().create()
val currencyAPIResponse = gson.fromJson(responseBody.string(), CurrencyAPIResponse::class.java)
val data: MutableState<List<CurrencyRates>> = currencyAPIResponse.data
Log.d("Data", data.value[0].code)
}
}
Everytime, I am getting the below error,
Attempt to invoke virtual method 'androidx.compose.runtime.MutableState com.tuts.playlite.network.response.CurrencyAPIResponse.getData()' on a null object reference
Not sure, where I am failing, I have tried to convert this to JSON Object, but still failing. Is it right way to get the data?
Another thing noticed that even though the JSON file is complete in the url, the response body log is showing the JSON with some other content.
{
"code": "IMP",
"value": "0.722603"
},
{
"code": "INR",
[ 1631385414.170 12452:12478 D/
responsebody]
"value": "72.99465"
},
{
"code": "IQD",
"value": "1458.61356"
},
As a result, the GSON might not able to form the json and hence could be getting null exception.
Not sure why random text is getting added!
You are already providing a Gson converter to retrofit, so retrofit should already be able to do the json to object conversion for you. That's the beauty of retrofit!
Try rewriting your CurrencyService like this:
interface CurrencyService {
#GET("v0/b/culoader.appspot.com/o/json%2Fcudata.json?alt=media&token=d0679703-2f6c-440f-af03-d4d61305cc84")
suspend fun getCurrencyFile(): CurrencyAPIResponse
}
Your ViewModel also has some issues. Not sure if you actually meant MutableState but I guess you're looking for MutableLiveData or MutableStateFlow. Below an example using MutableLiveData.
#HiltViewModel
class CurrencyDataViewModel #Injectconstructor(
private val currencyService: CurrencyService
) : ViewModel() {
private val _currencyData = MutableLiveData<List<CurrencyRates>>()
private val currencyData: LiveData = _currencyData
init {
getCurrencyFileData()
}
fun getCurrencyFileData() = viewModelScope.launch(Dispatchers.IO) {
_currencyData.postValue(currencyService.getCurrencyFile().data)
}
}
Use Kotin Coroutines for retorfit, Try something like below
interface CurrencyService {
#GET("v0/b/culoader.appspot.com/o/json%2Fcudata.json?alt=media&token=d0679703-2f6c-440f-af03-d4d61305cc84")
suspend fun getCurrencyFile(): Response<CurrencyAPIResponse>
}
and if you are using MVVM use this is repository class
suspend fun getCurrencyFile:Response<CurrencyAPIResponse>{
return currencyService.getCurrencyFile()
}
then in your view model class
Coroutines.main {
try{
val response = repository.getCurrencyFile()
if(response.isSuccessful){
//response.body is your data
}
}catch(Exception){}
}
if you are not using the repository class you can skip the second step and directly call the service in viewmodel class,
The Coroutines code is
object Coroutines {
fun main(work:suspend (()->Unit)) =
CoroutineScope(Dispatchers.Main).launch {
work()
}
}
Finally, the culprit seems to be responseBody Stream. After I changed the code with below, It seems to be working. I assume, it was unable to get the proper complete JSON earlier and throwing the null object reference error.
val responseBody = currencyService.getCurrencyFile(url).body()
val resData = responseBody?.byteStream()!!.bufferedReader().use {
it.readText()
}
val gson = GsonBuilder().setPrettyPrinting().create()
val currencyAPIResponse = gson.fromJson(resData, CurrencyAPIResponse::class.java)
Thank you for all support!
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 am getting the following error Unable to invoke no-args constructor for class Entry. Registering an InstanceCreator with Gson for this type may fix this problem.
My code extends an Entry class.
abstract class Data : ArrayList<Entry>() {
//code removed for simplicity
abstract class Entry(str: String) {
open var name: String = str
}
}
class Category: Data() {
class CategoryItem(
override var name: String
) : Entry(name)
}
I have a handler class to handle the transaction.
class JsonHelper(private var type: KClass<Category>) {
private val json = Gson()
private var contents: String? = null
private var data: Any? = null
fun convert(string: String): Any? {
data = json.fromJson(string, type.java)
return data
}
}
I call
json = JsonHelper(Category::class)
json.convert(file.read())
//file.read() comes from a file as a string, this works as intended
previously I had it setup where Entry was just an empty class and I added the name field while debugging, but it had no effect.
I have searched on stackoverflow and haven't found anything that would help, except that you shouldn't use suspend to call it, which you can see im not.
I have also tried using a TypeToken which I would pass into json.fromJson(string, type) at runtime, this also didn't help.
private var type = object: TypeToken<Category>(){}.type
Any Ideas? I would like gson to return a Category type class to me.
Well, Category extends ArrayList<Entry>, so GSON needs to know how to instantiate an Entry and it can't because it's abstract.
If the intention is that a Category should only contain CategoryItem, not arbitrary Entry, it should be
abstract class Data<T : Entry> : ArrayList<T>() {
abstract class Entry(str: String) {
open var name: String = str
}
}
class Category: Data<CategoryItem>() {
class CategoryItem(
override var name: String = ""
) : Entry(name)
}
Extending ArrayList instead of having a field of that type is also not usually a good idea.
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()