I use Moshi and I need to solve my problem with a buggy backend. Sometimes, when I request a list of objects, some of them don't contain mandatory fields. Of course, I can catch and process JsonDataException, but I want to skip these objects. How can I do it with Moshi?
Update
I have a couple of models for my task
#JsonClass(generateAdapter = true)
data class User(
val name: String,
val age: Int?
)
#JsonClass(generateAdapter = true)
data class UserList(val list: List<User>)
and buggy JSON
{
"list": [
{
"name": "John",
"age": 20
},
{
"age": 18
},
{
"name": "Jane",
"age": 21
}
]
}
as you can see, the second object has no mandatory name field and I want to skip it via Moshi adapter.
There's a gotcha in the solution that only catches and ignores after failure. If your element adapter stopped reading after an error, the reader might be in the middle of reading a nested object, for example, and then the next hasNext call will be called in the wrong place.
As Jesse mentioned, you can peek and skip the entire value.
class SkipBadElementsListAdapter(private val elementAdapter: JsonAdapter<Any?>) :
JsonAdapter<List<Any?>>() {
object Factory : JsonAdapter.Factory {
override fun create(type: Type, annotations: Set<Annotation>, moshi: Moshi): JsonAdapter<*>? {
if (annotations.isNotEmpty() || Types.getRawType(type) != List::class.java) {
return null
}
val elementType = Types.collectionElementType(type, List::class.java)
val elementAdapter = moshi.adapter<Any?>(elementType)
return SkipBadElementsListAdapter(elementAdapter)
}
}
override fun fromJson(reader: JsonReader): List<Any?>? {
val result = mutableListOf<Any?>()
reader.beginArray()
while (reader.hasNext()) {
try {
val peeked = reader.peekJson()
result += elementAdapter.fromJson(peeked)
} catch (ignored: JsonDataException) {
}
reader.skipValue()
}
reader.endArray()
return result
}
override fun toJson(writer: JsonWriter, value: List<Any?>?) {
if (value == null) {
throw NullPointerException("value was null! Wrap in .nullSafe() to write nullable values.")
}
writer.beginArray()
for (i in value.indices) {
elementAdapter.toJson(writer, value[i])
}
writer.endArray()
}
}
It seems I've found the answer
class SkipBadListObjectsAdapterFactory : JsonAdapter.Factory {
override fun create(type: Type, annotations: MutableSet<out Annotation>, moshi: Moshi): JsonAdapter<*>? {
return if (annotations.isEmpty() && Types.getRawType(type) == List::class.java) {
val elementType = Types.collectionElementType(type, List::class.java)
val elementAdapter = moshi.adapter<Any>(elementType)
SkipBadListObjectsAdapter(elementAdapter)
} else {
null
}
}
private class SkipBadListObjectsAdapter<T : Any>(private val elementAdapter: JsonAdapter<T>) :
JsonAdapter<List<T>>() {
override fun fromJson(reader: JsonReader): List<T>? {
val goodObjectsList = mutableListOf<T>()
reader.beginArray()
while (reader.hasNext()) {
try {
elementAdapter.fromJson(reader)?.let(goodObjectsList::add)
} catch (e: JsonDataException) {
// Skip bad element ;)
}
}
reader.endArray()
return goodObjectsList
}
override fun toJson(writer: JsonWriter, value: List<T>?) {
throw UnsupportedOperationException("SkipBadListObjectsAdapter is only used to deserialize objects")
}
}
}
Thank you "guys from the other topics" =)
You can find a working solution here:
https://github.com/square/moshi/issues/1288
Happy fixing :)
Related
When I enter the city name correctly everything goes fine but when the user enters the wrong city name it causes this error
{
"error": {
"code": 1006,
"message": "No matching locations found."}
}
How can I handle this error?
Api
interface Api{
#GET("forecast.json")
suspend fun getCurrentTemp(#Query("key")key : String, #Query("q")q: String,
#Query("days")days : Int): Response<Weatherapi>
companion object {
operator fun invoke(
):Api{
return Retrofit.Builder().baseUrl("https://api.weatherapi.com/v1/")
.addConverterFactory(GsonConverterFactory.create())
.build().create(Api::class.java)
}
}
}
Repository:
abstract class repositoryApi {
suspend fun <T : Any> CustomResponse(work: () ->Response <T>): T {
val response: Response<T> = work.invoke()
if (response.isSuccessful)
return response.body()!!
throw Exception(response.message())
}
}
handelRequst:
object handelRequst: repositoryApi() {
suspend fun <T:Any> Requst (response: Response<T>) = CustomResponse { response } }
handelCoroutins:
object handelCoroutins {
fun <T:Any> ThreadMain(work:suspend (() -> T) ,callback : ((T) -> Unit),ErrorMessage :
((String) -> Unit))=
CoroutineScope(Dispatchers.Main).launch {
try{
val data :T = CoroutineScope(Dispatchers.IO).async rt#{
return#rt work()
}.await()
callback(data)
}catch(e : IOException){
ErrorMessage.invoke("Error C")
}
}
}
viewModel:
class viewModelapi: ViewModel() {
val LivedataErrorhandel = MutableLiveData<String>()
var weather = MutableLiveData<Weatherapi>()
lateinit var job: Job
fun Gethome(key :String , q :String ,days :Int) {
try {
job = handelCoroutins.ThreadMain(
{
handelRequst.Requst(Api.invoke().getCurrentTemp(key ,q ,days))
},
{
weather.value = it
}, {
LivedataErrorhandel.value = it
}
)
} catch (e: IOException) {
LivedataErrorhandel.value = "Error C"
}
}
}
main Activity :
viewmodel.weather.observe(requireActivity(), Observer{
textViewtemp.text = it.current.temp_c.toString()
}
I'm not giving the full answer I just give you an idea of how you can handle this. Here's some code you might look at carefully I hope you can take this your way.
if (response.isSuccessful) {
return response.body()!!
} else {
//this is a json object that you should return for handle error
var error:JSONObject? = null
try {
//heres I convert error response to json object
error = JSONObject(response.errorBody()!!.charStream().readText())
//you may know how can you get this exception on your api implementation area
throw CustomException(error)
} catch (e: JSONException) {
throw Exception("Something is wrong !! ")
}
}
CustomException class
class CustomException(error:JsonObject):Exception()
heres how you should implement
try {
job = handelCoroutins.ThreadMain(
{
handelRequst.Requst(Api.invoke().getCurrentTemp(key ,q ,days))
},
{
weather.value = it
}, {
LivedataErrorhandel.value = it
}
)
} catch (e: IOException) {
LivedataErrorhandel.value = "Error C"
}catch(error:CustomException){
//heres you got the json object }
I have a response from this api, and there is different response on
...
"value": [
{
"#unit": "C",
"#text": "28"
}
]
sometimes
"value":
{
"#unit": "C",
"#text": "28"
}
I have try create this json adapter & model class from the answer
object WeatherResponse {
open class CuacaResponse{
#SerializedName("Success")
val success : Boolean = false
val row : RowBean? = null
}
data class RowBean(
val data : DataBean? = null
)
data class DataBean (
val forecast : ForecastBean? = null
)
data class ForecastBean(
val area : List<Area>? = null
)
data class Area(
#SerializedName("#id")
val id :String?="",
#SerializedName("#description")
val nama :String?="",
val parameter : List<DataMain>?=null
)
data class DataMain(
#SerializedName("#description")
val namaData :String?="",
#SerializedName("#id")
val id :String?="",
#SerializedName("timerange")
val timeRange : List<TimeRangeItem>
)
data class TimeRangeItem(
// sample data : 202107241800 => 2021-07-24-18:00
#SerializedName("#datetime")
val datetime : String,
#JsonAdapter(ValueClassTypeAdapter::class)
val value : ArrayList<ValueData>? = null,
)
data class ValueData(
#SerializedName("#unit")
val unit :String?="",
#SerializedName("#text")
val value :String?="",
)
class ValueClassTypeAdapter :
JsonDeserializer<ArrayList<ValueData?>?> {
override fun deserialize(
json: JsonElement,
typeOfT: Type?,
ctx: JsonDeserializationContext
): ArrayList<ValueData?> {
return getJSONArray(json, ValueData::class.java, ctx)
}
private fun <T> getJSONArray(json: JsonElement, type: Type, ctx:
JsonDeserializationContext): ArrayList<T> {
val list = ArrayList<T>()
if (json.isJsonArray) {
for (e in json.asJsonArray) {
list.add(ctx.deserialize<Any>(e, type) as T)
}
} else if (json.isJsonObject) {
list.add(ctx.deserialize<Any>(json, type) as T)
} else {
throw RuntimeException("Unexpected JSON type: " + json.javaClass)
}
return list
}
}
}
my retrofit service :
interface WeatherService {
#GET("/api/cuaca/DigitalForecast-{province}.xml?format=json")
suspend fun getWeather(
#Path("province") provinceName: String? = "JawaTengah"
) : WeatherResponse.CuacaResponse?
companion object {
private const val URL = "https://cuaca.umkt.ac.id"
fun client(context: Context): WeatherService {
val httpClient = OkHttpClient.Builder()
httpClient.apply {
addNetworkInterceptor(
ChuckerInterceptor(
context = context,
alwaysReadResponseBody = true
)
)
addInterceptor { chain ->
val req = chain.request()
.newBuilder()
.build()
return#addInterceptor chain.proceed(req)
}
cache(null)
}
val gsonConverterFactory = GsonConverterFactory.create()
return Retrofit.Builder()
.baseUrl(URL)
.client(httpClient.build())
.addConverterFactory(gsonConverterFactory)
.build()
.create(WeatherService::class.java)
}
}
}
But the result that i got, from log request :
...
{
"#datetime": "202108070000",
"value": {
"size": 4
}
},
{
"#datetime": "202108070600",
"value": {
"size": 4
}
},
{
"#datetime": "202108071200",
"value": {
"size": 4
}
}
...
the value return size that IDK from where, it should return array of unit & text from the api.
Please anyone help me from this stuck, thanks in advance!
Finaly, i have solved this by change JsonDeserializer to TypeAdapterFactory as mentioned on this answer
I am consuming a rest API that uses cursor based pagination to show some results. I am wondering if I can use Paging Library 3.0 to paginate it. I have been looking through some mediums and docs and can't seem to find a way to implement it. If any of you has come around any solution, I would be so happy to hear from it!
The api response pagination looks like this:
"paging": {
"previous": false,
"next": "https://api.acelerala.com/v1/orders/?store_id=4&after=xyz",
"cursors": {
"before": false,
"after": "xyz"
}
}
In kotlin, here example.
In Activity or somewhere:
viewModel.triggerGetMoreData("data").collectLatest {
mAdapter.submitData(it)
}
In viewModel:
fun triggerGetMoreData(data: String): Flow<PagingData<SampleData>> {
val request = ExampleRequest(data)
return exampleRepository.getMoreData(request).cachedIn(viewModelScope)
}
In Repository:
fun getMoreData(request: ExampleRequest): Flow<PagingData<ExampleData>> {
return Pager(
config = PagingConfig(
pageSize = 30,
enablePlaceholders = false
),
pagingSourceFactory = { ExamplePagingSource(service, request) }
).flow
}
and
class ExamplePagingSource (
private val service: ExampleService,
private val request: ExampleRequest): PagingSource<Int, ExampleData>() {
override suspend fun load(params: LoadParams<Int>): LoadResult<Int, ExampleData> {
return try {
val pageIndex = params.key ?: 0
val request = request.copy(index = (request.pageNum.toInt() * pageIndex).toString())
when (val result = service.getMoreData(request)) { // call api
is NetworkResponse.Success -> {
val listData = result.body.items?.toData()?: listOf()
LoadResult.Page(
data = listData,
prevKey = if (pageIndex == 0) null else pageIndex - 1,
nextKey = if (listData.isEmpty()) null else pageIndex + 1
)
}
else -> LoadResult.Error(result.toError())
}
} catch (e: Exception) {
e.printStackTrace()
LoadResult.Error(e)
}
}
}
Thanks to #Đặng Anh Hào I was able to get on track. As my cursor is a String and not at Int, the Paging Source load function looks like this:
override suspend fun load(params: LoadParams<String>): LoadResult<String, Order> {
return try{
val response = service.getOrders(query,params.key?:"",10)
val nextKey = if(response.paging?.cursors?.after=="false") null else response.paging?.cursors?.after
val prevKey = if(response.paging?.cursors?.before=="false") null else response.paging?.cursors?.before
LoadResult.Page(response.data?.toOrderList()?:emptyList(),prevKey,nextKey)
}catch (exception: IOException) {
LoadResult.Error(exception)
} catch (exception: retrofit2.HttpException) {
LoadResult.Error(exception)
}
}
and the onrefreshkey looks like this:
override fun getRefreshKey(state: PagingState<String, Order>): String? {
return state.anchorPosition?.let {
state.closestItemToPosition(it)?.orderId
}
}
The repository method looks like this:
fun getOrdersPaginated(storeId: String): Flow<PagingData<Order>> {
return Pager(
config = PagingConfig(enablePlaceholders = false,pageSize = 10),
pagingSourceFactory = {PagingSource(apiService,storeId)}
).flow
}
And the View Model method is like this:
private val _pagedOrders = MutableLiveData<PagingData<Order>>()
val orders get() = _pagedOrders
private var currentQueryValue: String? = null
private var currentSearchResult: Flow<PagingData<Order>>? = null
fun getOrdersPaginated(storeId: String) {
viewModelScope.launch {
currentQueryValue = storeId
val newResult: Flow<PagingData<Order>> = repository.getOrdersPaginated(storeId)
.cachedIn(viewModelScope)
currentSearchResult = newResult
currentSearchResult!!.collect {
_pagedOrders.value = it
}
}
}
The activity calls the paging like this:
private var searchJob: Job? = null
private fun getOrders() {
viewModel.getOrdersPaginated(storeId)
}
private fun listenForChanges() {
viewModel.orders.observe(this, {
searchJob?.cancel()
searchJob = lifecycleScope.launch {
ordersAdapter.submitData(it)
}
})
}
And finally the adapter is the same as a ListAdapter, the only thing that changes is that it now extends PagingDataAdapter<Order, OrderAdapter.ViewHolder>(OrdersDiffer)
For a more detailed tutorial on how to do it, I read this codelab
I have trouble to parse some inner part of JSON (with Moshi) that can vary from a lot and is highly unstructured. Overall it looks like:
response: {
items: [{
type: "typeA",
data: {
"1563050214700-001": {
foo: 123 ....
}
}
}, {
type: "typeB",
data: {
"1563050214700-002": {[
// differs a lot from previous one
{bar: 123 .... }
]}
}
}]
}
And data class structure looks like:
data class Response(
val items: Map<String,List<Item?>>?
) {
data class Item(
val type: String?,
val data: Map<String,List<DataItem?>>?
) {
data class DataItem(
// members highly unstructured
)
}
}
Schema of "DataItem" varies a lot. Looks like Moshi codegen supports adapters that can potentially allow manual parsing of these inner data classes but I'm not able to find the right tutorial or example. Ideally, I want entire Response parsed just as if it were a well-defined JSON.
Here is how I use retrofit/moshi
#Provides
#Singleton
#MyApp
fun provideMyAppRetrofit(context: Context, #MyApp okHttpClient: OkHttpClient): Retrofit {
return Retrofit.Builder()
.client(okHttpClient)
.addConverterFactory(MoshiConverterFactory.create())
.addCallAdapterFactory(RxJava2CallAdapterFactory.create())
.baseUrl(context.getString(R.string.APP_BASE_URL))
.build()
}
#Provides
#Singleton
fun provideMyAppApiService(#MyApp retrofit: Retrofit): MyAppApiService {
return retrofit.create(MyAppApiService::class.java)
}
How do I achieve this? Any sample or reference implementation will be helpful.
welcome to the polymorphic JSON parsing problems word!
We were writing own JSON adapters that are looking like:
internal class CardJsonAdapter(
moshi: Moshi
) : JsonAdapter<Card>() {
private val cardTypeAdapter = moshi.adapter(Card.Type::class.java)
private val amountsWithActionAdapter = moshi.adapter(AmountsWithActionCard::class.java)
private val backgroundImageCardAdapter = moshi.adapter(BackgroundImageCard::class.java)
#Suppress("TooGenericExceptionCaught")
override fun fromJson(reader: JsonReader): Card = try {
#Suppress("UNCHECKED_CAST")
val jsonMap = reader.readJsonValue() as Map<String, Any?>
val type = cardTypeAdapter.fromJsonValue(jsonMap["type"])
createCardWithType(type, jsonMap)
} catch (error: Exception) {
if (BuildConfig.DEBUG) {
Log.w("CardJsonAdapter", "Failed to parse card", error)
}
// Try not to break the app if we get unexpected data: ignore errors and return a placeholder card instead.
UnknownCard
}
override fun toJson(writer: JsonWriter, value: Card?) {
throw NotImplementedError("This adapter cannot write cards to JSON")
}
private fun createCardWithType(type: Type?, jsonMap: Map<String, Any?>) = when (type) {
null -> UnknownCard
Type.AMOUNTS_WITH_ACTION -> amountsWithActionAdapter.fromJsonValue(jsonMap)!!
Type.BACKGROUND_IMAGE_WITH_TITLE_AND_MESSAGE -> backgroundImageCardAdapter.fromJsonValue(jsonMap)!!
}
}
However, it is not anymore required. Moshi supports polymorphic JSON parsing now - https://proandroiddev.com/moshi-polymorphic-adapter-is-d25deebbd7c5
Make a custom adapter
class YourAdapter {
#FromJson
fun fromJson(reader: JsonReader, itemsAdapter: JsonAdapter<ItemsResponse>): List<ItemsResponse>? {
val list = ArrayList<ItemsResponse>()
if (reader.hasNext()) {
val token = reader.peek()
if (token == JsonReader.Token.BEGIN_ARRAY) {
reader.beginArray()
while (reader.hasNext()) {
val itemResponse = itemsAdapter.fromJsonValue(reader.readJsonValue())
itemsResponse?.let {
list.add(itemResponse)
}
}
reader.endArray()
}
}
return list
}
}
The following will produce an IllegalArgumentException because you "Cannot serialize abstract class"
sealed class Animal {
data class Dog(val isGoodBoy: Boolean) : Animal()
data class Cat(val remainingLives: Int) : Animal()
}
private val moshi = Moshi.Builder()
.build()
#Test
fun test() {
val animal: Animal = Animal.Dog(true)
println(moshi.adapter(Animal::class.java).toJson(animal))
}
I have tried solving this using a custom adapter, but the only solution I could figure out involves explicitly writing all of the property names for each subclass. e.g:
class AnimalAdapter {
#ToJson
fun toJson(jsonWriter: JsonWriter, animal: Animal) {
jsonWriter.beginObject()
jsonWriter.name("type")
when (animal) {
is Animal.Dog -> jsonWriter.value("dog")
is Animal.Cat -> jsonWriter.value("cat")
}
jsonWriter.name("properties").beginObject()
when (animal) {
is Animal.Dog -> jsonWriter.name("isGoodBoy").value(animal.isGoodBoy)
is Animal.Cat -> jsonWriter.name("remainingLives").value(animal.remainingLives)
}
jsonWriter.endObject().endObject()
}
....
}
Ultimately I'm looking to produce JSON that looks like this:
{
"type" : "cat",
"properties" : {
"remainingLives" : 6
}
}
{
"type" : "dog",
"properties" : {
"isGoodBoy" : true
}
}
I'm happy with having to use the custom adapter to write the name of each type, but I need a solution that will automatically serialize the properties for each type rather than having to write them all manually.
This can be done with PolymorphicJsonAdapterFactory and including an extra property in the json to specify the type.
For example:
This JSON
{
"animals": [
{
"type": "dog",
"isGoodBoy": true
},
{
"type": "cat",
"remainingLives": 9
}
]
}
Can be mapped to the following classes
sealed class Animal {
#JsonClass(generateAdapter = true)
data class Dog(val isGoodBoy: Boolean) : Animal()
#JsonClass(generateAdapter = true)
data class Cat(val remainingLives: Int) : Animal()
object Unknown : Animal()
}
With the following Moshi config
Moshi.Builder()
.add(
PolymorphicJsonAdapterFactory.of(Animal::class.java, "type")
.withSubtype(Animal.Dog::class.java, "dog")
.withSubtype(Animal.Cat::class.java, "cat")
.withDefaultValue(Animal.Unknown)
)
I think you need the polymorphic adapter to achieve this which requires the moshi-adapters artifact. This will enable serialization of sealed classes with different properties. More details are in this article here: https://proandroiddev.com/moshi-polymorphic-adapter-is-d25deebbd7c5
I have solved this by creating a Factory, an enclosing class, and an enum that can provide the classes for each item type. However this feels rather clunky and I would love a more straight forward solution.
data class AnimalObject(val type: AnimalType, val properties: Animal)
enum class AnimalType(val derivedClass: Class<out Animal>) {
DOG(Animal.Dog::class.java),
CAT(Animal.Cat::class.java)
}
class AnimalFactory : JsonAdapter.Factory {
override fun create(type: Type, annotations: MutableSet<out Annotation>, moshi: Moshi): JsonAdapter<AnimalObject>? {
if (!Types.getRawType(type).isAssignableFrom(AnimalObject::class.java)) {
return null
}
return object : JsonAdapter<AnimalObject>() {
private val animalTypeAdapter = moshi.adapter<AnimalType>(AnimalType::class.java)
override fun fromJson(reader: JsonReader): AnimalObject? {
TODO()
}
override fun toJson(writer: JsonWriter, value: AnimalObject?) {
writer.beginObject()
writer.name("type")
animalTypeAdapter.toJson(writer, value!!.type)
writer.name("properties")
moshi.adapter<Animal>(value.type.derivedClass).toJson(writer, value.properties)
writer.endObject()
}
}
}
}
Answer is taken from: github.com/square/moshi/issues/813
You should be able to create your own JsonAdapter.Factory and provide custom adapter whenever an Animal need to be serialized/deserialized:
sealed class Animal {
#JsonClass(generateAdapter = true)
data class Dog(val isGoodBoy: Boolean) : Animal()
#JsonClass(generateAdapter = true)
data class Cat(val remainingLives: Int) : Animal()
}
object AnimalAdapterFactory : JsonAdapter.Factory {
override fun create(type: Type, annotations: MutableSet<out Annotation>, moshi: Moshi): JsonAdapter<*>? =
when (type) {
Animal::class.java -> AnimalAdapter(moshi)
else -> null
}
private class AnimalAdapter(moshi: Moshi) : JsonAdapter<Animal>() {
private val mapAdapter: JsonAdapter<MutableMap<String, Any?>> =
moshi.adapter(Types.newParameterizedType(Map::class.java, String::class.java, Any::class.java))
private val dogAdapter = moshi.adapter(Animal.Dog::class.java)
private val catAdapter = moshi.adapter(Animal.Cat::class.java)
override fun fromJson(reader: JsonReader): Animal? {
val mapValues = mapAdapter.fromJson(reader)
val type = mapValues?.get("type") ?: throw Util.missingProperty("type", "type", reader)
val properties = mapValues["properties"] ?: throw Util.missingProperty("properties", "properties", reader)
return when (type) {
"dog" -> dogAdapter.fromJsonValue(properties)
"cat" -> catAdapter.fromJsonValue(properties)
else -> null
}
}
override fun toJson(writer: JsonWriter, value: Animal?) {
writer.beginObject()
writer.name("type")
when (value) {
is Animal.Dog -> writer.value("dog")
is Animal.Cat -> writer.value("cat")
}
writer.name("properties")
when (value) {
is Animal.Dog -> dogAdapter.toJson(writer, value)
is Animal.Cat -> catAdapter.toJson(writer, value)
}
writer.endObject()
}
}
}
private val moshi = Moshi.Builder()
.add(AnimalAdapterFactory)
.build()
#Test
fun test() {
val dog: Animal = Animal.Dog(true)
val cat: Animal = Animal.Cat(7)
println(moshi.adapter(Animal::class.java).toJson(dog))
println(moshi.adapter(Animal::class.java).toJson(cat))
val shouldBeDog: Animal? = moshi.adapter(Animal::class.java).fromJson(moshi.adapter(Animal::class.java).toJson(dog))
val shouldBeCat: Animal? = moshi.adapter(Animal::class.java).fromJson(moshi.adapter(Animal::class.java).toJson(cat))
println(shouldBeDog)
println(shouldBeCat)
}