Moshi create json with dynamic inner data class - android

I have moshi with retrofit and I want to send dynamic inner data object to backend.
Lets say I need to patch some data on my endpoint:
#PATCH("/animals/{animalId}/attributes")
suspend fun updateAnimal(#Path("animalId") animalId: String, #Body request: MyPetsRequest)
But my AnimalAttributes inner object is dynamic.
#JsonClass(generateAdapter = true)
#Parcelize
data class MyPetsRequest(
#Json(name = "name") val name: String,
#Json(name = "attributes") val attributes: AnimalAttributes
) : Parcelable
interface AnimalAttributes : Parcelable // or sealed class possible when share same base fields
#JsonClass(generateAdapter = true)
#Parcelize
data class DogAttributes(
#Json(name = "bark") val bark: Boolean,
#Json(name = "bite") val bite: Boolean,
) : AnimalAttributes
#JsonClass(generateAdapter = true)
#Parcelize
data class CatAttributes(
#Json(name = "weight") val weight: Int,
) : AnimalAttributes
I tried create a custom adapter for Moshi, but when I use #ToJson inner object attributes data are escaped (so complete object is send as one parameter).
class AnimalAttributesAdapter {
#ToJson
fun toJson(value: AnimalAttributes): String {
val moshi = Moshi.Builder().build()
return when (value) {
is DogAttributes -> moshi.adapter(DogAttributes::class.java).toJson(value)
is CatAttributes -> moshi.adapter(CatAttributes::class.java).toJson(value)
else -> throw UnsupportedOperationException("Unknown type to serialize object to json: $value")
}
}
/**
* Not supported, this object is used only one way to update data on server.
*/
#FromJson
fun fromJson(value: String): AnimalAttributes = throw UnsupportedOperationException()
}
Moshi.Builder().add(AnimalAttributesAdapter())
Result in this case looks like:
{"name":"my pet name","attributes":"{\"bark\":\"true\", \"bite\":\"false\"}"}
When I try to use PolymorphicJsonAdapterFactory it added next parameter between attributes which is not acceptable for my use case.
PolymorphicJsonAdapterFactory.of(AnimalAttributes::class.java, "animal")
.withSubtype(DogAttributes::class.java, "dog")
.withSubtype(CatAttributes::class.java, "cat")
Result in this case added attribute animal: {"name":"my pet name","attributes":{"animal":"dog","bark":true, "bite":false}}
I don't need to deserialize object from json to data class. I only need create one way serialization to json.

Related

Creating json from generic strings

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

Moshi cannot parse nullable

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

How to encode a field of an object as stringifed JSON instead of nested JSON object in Moshi?

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

How to parcelise member variable other than constructor in data class while using #Parcelize

I am using Room and Kotlin data class. Such as,
#Entity(tableName = "Person")
#Parcelize
class Test(#ColumnInfo(name = "name") var name:String) : Parcelable{
#PrimaryKey(autoGenerate = true)
#ColumnInfo(name = "ID")
var id: Long? = null
}
I can create the instance using the constructor and insert the data. I am also getting a warning "property would not be serialized into a 'parcel'". When I was trying to send the object through a bundle, the id is missing, which is expected as the warning says so. How can I add that member ID in the parcel? I am not keeping the ID in the constructor as I want them to be generated automatically.
You can find this information with the documentation:
#Parcelize requires all serialized properties to be declared in the primary constructor. Android Extensions will issue a warning on each property with a backing field declared in the class body. Also, #Parcelize can't be applied if some of the primary constructor parameters are not properties.
If your class requires more advanced serialization logic, you can write it inside a companion class:
#Parcelize
data class User(val firstName: String, val lastName: String, val age: Int) : Parcelable {
private companion object : Parceler<User> {
override fun User.write(parcel: Parcel, flags: Int) {
// Custom write implementation
}
override fun create(parcel: Parcel): User {
// Custom read implementation
}
}
}

Embedded list - how to?

#Entity
data class Products(
val id:String,
val title: String,
val description: String,
val imgUrl: String,
val usageRules : List<String>, //what happens here?
)
i am playing around with ROOM for android and most of the examples i have seen does not explain how you would create a Entity table that has a arrayList within it.
There is #Embedded which seems to only nest another object but i am trying to nest a LIST of objects called usageRules
Do i need to create a seperate Usage Rule table? The thing is, each usage rule can be included on multiple products above and also a product can have multiple usage rules defined so it is a many to many relationship.
Is this possible with ROOM?
I know that in Realm i can simply convert the UsageRule List<> to a RealmList<>. is there an equivalent in ROOM?
Another alternative is that because it is just a lis of strings. i could simply create a new table entity field called allUsage that stores all the usageRules on a particular product with a seperator and then later when i want to construct the data object again from ROOM i can grab its contents by doing allUsage.Split(..)
Still would rather do it another way as i may come across a scenarionwhere the usageRules are not just a list of strings but a list of objects....
Try using ArrayList:-
#Entity
data class Products(
val id:String,
val title: String,
val description: String,
val imgUrl: String,
val usageRules : ArrayList<String>, //what happens here?
)
Need to use Convertors for this:-
class Converters {
companion object {
#TypeConverter
#JvmStatic
fun fromString(value: String): ArrayList<String>? {
val listType = object : TypeToken<ArrayList<String>>() {}.type
return Gson().fromJson(value, listType)
}
#TypeConverter
#JvmStatic
fun fromArrayList(list: ArrayList<String>?): String {
val gSon = Gson()
return gSon.toJson(list)
}
}
}
And define Convertors Here:-
#Database(entities = [TableModel::class], version = 1, exportSchema = false)
#TypeConverters(Converters::class)
abstract class RDatabase : RoomDatabase() {
abstract val tableDAO: TableDAO
}
One extra implementation needs:-
implementation 'com.google.code.gson:gson:2.8.5'
My code is perfectly working by this

Categories

Resources