Serialize sealed class with Moshi - android

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)
}

Related

Combine LiveData or Flow based on the first LiveData or Flow value

I would like to combine two livedata / flow values conditionally. Here is my problem: Currently, I have two livedata / flow values. One LiveData value always emits type Status<Unit> while the second LiveData value emits T. When the first LiveData value emits Status.Success I manually set View to visible and now know that the second LiveData value will emit T.
What I now want is, to get the second Livedata value T inside my first LiveData value onSucess block
Current approach
class MyViewModel() : ViewModel() {
val myDownloadState: LiveData<Status<Unit>> = ...
val myDownloadData: LiveData<T> = ...
}
class MyFragment : Fragment() {
val myViewModel = ...
myViewModel.myDownloadState.observeStatus(
viewLifecycleOwner,
onSuccess = { it: Unit
binding.myRecyclerView.isVisible = true
},
onLoading = {
binding.myRecyclerView.isVisible = false
},
onError = { it: String?
toast(it.toString())
}
)
myViewModel.myDownloadData.observe(viewLifecycleOwner) { data: T
binding.myRecylerView.submitList(data)
}
}
What I want
class MyViewModel() : ViewModel() {
val myCombinedState: LiveData<Status<T>> = ...
}
class MyFragment : Fragment() {
val myViewModel = ...
myViewModel.myCombinedState.observeStatus(
viewLifecycleOwner,
onSuccess = { it: T
binding.myRecyclerView.isVisible = true
binding.myRecylerView.submitList(data)
},
onLoading = {
binding.myRecyclerView.isVisible = false
},
onError = { it: String?
toast(it.toString())
}
)
}
Here is where the two livedata values are coming from:
interface IWorkerContract<T, R> {
// this is "myDownloadData"
val appDatabaseData: LiveData<R>
// this is "myDownloadState"
val workInfo: LiveData<Status<Unit>>
}
#Singleton
class DocumentWorkerContract #Inject constructor(
#ApplicationContext private val context: Context,
private val documentDao: DocumentDao,
) : IWorkerContract<Unit, List<DocumentCacheEntity>> {
// this is "myDownloadData"
override val appDatabaseData: LiveData<List<DocumentCacheEntity>>
get() = documentDao.getListLiveData()
// this is "myDownloadState"
override val workInfo: LiveData<Status<Unit>>
get() = WorkManager
.getInstance(context)
.getWorkInfoByIdLiveData(worker.id)
.mapToState()
}
State Class
sealed class Status<out T> {
data class Success<out T>(val data: T) : Status<T>()
class Loading<out T>(val message: String? = null) : Status<T>()
data class Failure<out T>(val message: String?) : Status<T>()
companion object {
fun <T> success(data: T) = Success(data)
fun <T> loading(message: String? = null) = Loading<T>(message)
fun <T> failed(message: String?) = Failure<T>(message)
}
}
I think you should try using switchMap in combination with map in this case.
Try it this way:
class MyViewModel() : ViewModel() {
val myCombineState: LiveData<List<DocumentCacheEntity>> = myDownloadState.switchMap { state ->
myDownloadData.map { data ->
when (state) {
is Status.Success -> {
Status.Success(data)
}
is Status.Loading -> {
Status.Loading()
}
is Status.Error -> {
Status.Error()
}
}
}
}
}

How to convert a string from Retrofit to a custom object in my data class model?

I am receiving a Category object from Retrofit:
data class Category(
val id: Int,
val title: String,
val type: CategoryType
)
data class CategoryList(
#SerializedName("categories")
val categories: List<Category>
)
where CategoryType is an enum class:
enum class CategoryType constructor(
var title: String
) {
CAT_CHICKEN("chicken"),
CAT_PORK("pork"),
CAT_STEAK("steak"),
companion object {
fun getCategoryTypeByName(text: String): CategoryType? {
return when (text) {
"chicken" -> CAT_CHICKEN
"pork" -> CAT_PORK
"steak" -> CAT_STEAK
else -> null
}
}
}
}
My api call is like this:
#GET("categs/melist/")
suspend fun getCategories(): Response<CategoryList>
How can I convert the 'type' variable that comes from the server as a string to a CategoryType object?
The problem is what your categories (for example "steak") doesn't match your enum values (for example CAT_STEAK). Use #SerializedName keyword:
enum class CategoryType constructor(var title: String) {
#SerializedName("chicken")
CAT_CHICKEN("chicken"),
#SerializedName("pork")
CAT_PORK("pork"),
#SerializedName("steak")
CAT_STEAK("steak"),
companion object {
fun getCategoryTypeByName(text: String): CategoryType? {
return when (text) {
"chicken" -> CAT_CHICKEN
"pork" -> CAT_PORK
"steak" -> CAT_STEAK
else -> null
}
}
}
}

Moshi adapter to skip bad objects in the List<T>

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 :)

RealmList with #Parcelize annotation

I'm trying to use the new #Parcelize annotation added with Kotlin 1.1.4 with a Realm objet containing a RealmList attribute.
#Parcelize
#RealmClass
open class Garage(
var name: String? = null,
var cars: RealmList<Car> = RealmList()
) : Parcelable, RealmModel
As RealmList is not supported by the annotation and assuming that #Parcelize is there specially to avoid creating methods what would be the solution to support RealmList ?
Thanks in advance
EDIT:
Using the power of Type Parcelers and #WriteWith annotation, it is possible to create a RealmList type parceler that can handle this scenario for you:
With the following code:
fun <T> Parcel.readRealmList(clazz: Class<T>): RealmList<T>?
where T : RealmModel,
T : Parcelable = when {
readInt() > 0 -> RealmList<T>().also { list ->
repeat(readInt()) {
list.add(readParcelable(clazz.classLoader))
}
}
else -> null
}
fun <T> Parcel.writeRealmList(realmList: RealmList<T>?, clazz: Class<T>)
where T : RealmModel,
T : Parcelable {
writeInt(when {
realmList == null -> 0
else -> 1
})
if (realmList != null) {
writeInt(realmList.size)
for (t in realmList) {
writeParcelable(t, 0)
}
}
}
You can define an interface like this:
interface RealmListParceler<T>: Parceler<RealmList<T>?> where T: RealmModel, T: Parcelable {
override fun create(parcel: Parcel): RealmList<T>? = parcel.readRealmList(clazz)
override fun RealmList<T>?.write(parcel: Parcel, flags: Int) {
parcel.writeRealmList(this, clazz)
}
val clazz : Class<T>
}
Where you'll need to create a specific parceler for the RealmList<Car> like this:
object CarRealmListParceler: RealmListParceler<Car> {
override val clazz: Class<Car>
get() = Car::class.java
}
but with that, now you can do the following:
#Parcelize
#RealmClass
open class Garage(
var name: String? = null,
var cars: #WriteWith<CarRealmListParceler> RealmList<Car> = RealmList()
) : Parcelable, RealmModel
And
#Parcelize
#RealmClass
open class Car(
..
): RealmModel, Parcelable {
...
}
This way you don't need to manually write the Parceler logic.
ORIGINAL ANSWER:
Following should work:
#Parcelize
open class Garage: RealmObject(), Parcelable {
var name: String? = null
var cars: RealmList<Car>? = null
companion object : Parceler<Garage> {
override fun Garage.write(parcel: Parcel, flags: Int) {
parcel.writeNullableString(name)
parcel.writeRealmList(cars)
}
override fun create(parcel: Parcel): Garage = Garage().apply {
name = parcel.readNullableString()
cars = parcel.readRealmList()
}
}
}
As long as you also add following extension functions:
inline fun <reified T> Parcel.writeRealmList(realmList: RealmList<T>?)
where T : RealmModel,
T : Parcelable {
writeInt(when {
realmList == null -> 0
else -> 1
})
if (realmList != null) {
writeInt(realmList.size)
for (t in realmList) {
writeParcelable(t, 0)
}
}
}
inline fun <reified T> Parcel.readRealmList(): RealmList<T>?
where T : RealmModel,
T : Parcelable = when {
readInt() > 0 -> RealmList<T>().also { list ->
repeat(readInt()) {
list.add(readParcelable(T::class.java.classLoader))
}
}
else -> null
}
and also
fun Parcel.writeNullableString(string: String?) {
writeInt(when {
string == null -> 0
else -> 1
})
if (string != null) {
writeString(string)
}
}
fun Parcel.readNullableString(): String? = when {
readInt() > 0 -> readString()
else -> null
}

Get generics class loader for parsing nested Parcelable generic field

I have a wrapper of Parcelable generic type but Parcel constructing fails to compile because T class can not be determined generically
class MyItem<T : Parcelable> (val model: T) : Parcelable {
constructor(parcel: Parcel) :
this(parcel.readParcelable(T::class.java.classLoader)) {
}
}
Is there any solution to this case?
In order to get the whole picture here is what I ended up with:
The use case is one has a Parcelable generic instance let's call it model which should be completed with common properties of Parcelable wrapper in order to not pollute the model with extrinsic fields. For example Item wrapper.
In the example below the wrapper extra property gives us some type of index :
class Item<T : Parcelable> (val model: T, val index: Int ) : Parcelable {
constructor(parcel: Parcel) :
this(parcel.readParcelable(
Item<T>::model.javaClass.classLoader),
parcel.readInt()
) {}
override fun writeToParcel(parcel: Parcel?, flag: Int) {
parcel?.writeParcelable(model, 0)
parcel?.writeInt(index)
}
override fun describeContents(): Int {
return 0
}
companion object CREATOR : Parcelable.Creator<Item<Parcelable>> {
override fun createFromParcel(parcel: Parcel): Item<Parcelable> {
return Item(parcel)
}
override fun newArray(size: Int): Array<Item<Parcelable>?> {
return arrayOfNulls(size)
}
}
}
So in the end we can have something like: Item(Person("John"), 0), Item(Person("Bill"), 1)...
class PersonPagerFragment() : BasePagerFragment<Person>() {
companion object {
fun newInstance(itemList: ArrayList<Item<Person>>)
: PersonPagerFragment {
val args = Bundle()
val fragment = PersonPagerFragment()
args.putParcelableArrayList("items", itemList)
fragment.arguments = args
return fragment
}
}
}
extending class like:
class BasePagerFragment<T : Parcelable> : Fragment(){
protected fun readBundle(bundle: Bundle?) {
bundle.getParcelableArrayList<Item<T>>("items")
}
}
You can use a reified inline function as a factory method to achieve this. I prefer to do this on a companion object. Here's an MCVE:
class MyItem<T> (val model: T) {
companion object {
inline fun <reified T> from(parcel : T) : MyItem<T> {
return MyItem<T>(T::class.java.newInstance())
}
}
}
fun main(args: Array<String>) {
val mi = MyItem.from("hi")
println("mi is a ${mi::class}")
}
If you need to have a Parcel-type constructor, you could change that to be the primary constructor, and figure out the type of the MyItem then.
class Parcel
class MyItem(val parcel: Parcel) {
inline fun <reified T> model() : T {
// You code would be calling
// `parcel.readParcelable(T::class.java.classLoader)`
return T::class.java.newInstance() as T
}
}
fun main(args: Array<String>) {
// You don't ned to know the out type when constructing MyItem.
val mi = MyItem(Parcel())
// But you do need to know it when calling model()
val model : String = mi.model()
println("mi.model is a ${model::class}")
}

Categories

Resources