Json same name different data type Moshi Android - android

This is my model class and i have these types of json to convert in to this model. How i can do that with Moshi (Using Retrofit)
data class(var Id:Int, var image:List<String>)
{"Id":188, "image":"\/posts\/5fd9aa6961c6dd54129f51d1.jpeg"}
{"Id":188, "image":["\/posts\/5fd9aa6961c6dd54129f51d1.jpeg","\/posts\/5fd9aa6961c6dd54129f51d1.jpeg"]}

Your case is a bit unorthodox, in general I would avoid designing JSONs with matching field names but different signature. Anyway, the solution:
Define your model as follow:
data class MyModel(
#Json(name = "Id") val Id: Long,
#Json(name = "image") val images: List<String>
)
Then, you will have to create a custom adapter for it:
class MyModelAdapter {
#ToJson
fun toJson(model: MyModel): String {
// MyModel is data class so .toString() should convert it to correct Json format with
// image property as list of image path strings
return model.toString()
}
#FromJson
fun fromJson(reader: JsonReader): MyModel = with(reader) {
// We need to manually parse the json
var id: Long? = null
var singleImage: String? = null
val imageList = mutableListOf<String>()
beginObject()
while (hasNext()) {
// iterate through the JSON fields
when (nextName()) {
"Id" -> id = nextLong() // map the id field
"image" -> { // map the image field
when (peek()) {
JsonReader.Token.BEGIN_ARRAY -> {
// the case where image field is an array
beginArray()
while(hasNext()) {
val imageFromList = nextString()
imageList.add(imageFromList)
}
endArray()
}
JsonReader.Token.STRING -> {
// the case where image field is single string
singleImage = nextString()
}
else -> skipValue()
}
}
else -> skipValue()
}
}
endObject()
id ?: throw IllegalArgumentException("Id should not be null")
val images = if (singleImage != null) {
listOf(singleImage)
} else {
imageList
}
MyModel(Id = id, images = images)
}
}
Then, add the adapter to your moshi builder:
Moshi.Builder()
.add(MyModelAdapter())
.build()
That should do it. For complete code base, you can check my demo I just created that mirrors your case:
https://github.com/phamtdat/MoshiMultipleJsonDemo

Related

Formatting the Nested JSON response from retrofit API in MVVM Architecture - Kotlin

I m new to kotlin and MVVM, I have been working around this issue for a week now, couldn't get any idea even after searching for some code on the internet.
I'm trying to edit or modify the retrofit response (to observe a specific type; say "sf") according to my need and neglecting other data which is not needed. I'm using mutable livedata to fetch and update the JSON data from the retrofit response to the recylerview.
Here is the link for the JSON data: http://www.nactem.ac.uk/software/acromine/dictionary.py?sf=HMM
3 Data classes based on JSON response:
data class sf(
#SerializedName("sf")
#Expose
val sf : String? = null,
#SerializedName("lfs")
#Expose
val lfs : List<lfs>? = null,
)
data class lfs(
#SerializedName("lf")
#Expose
var lf : String? = null,
#SerializedName("freq")
#Expose
var freq : Int? = null,
#SerializedName("since")
#Expose
var since : Int? = null,
#SerializedName("vars")
#Expose
var vars : List<vars>? = null,
) : Serializable
class vars (
#SerializedName("lf")
#Expose
var lf : String? = null,
#SerializedName("freq")
#Expose
var freq : Int? = null,
#SerializedName("since")
#Expose
var since : Int?
): Serializable
Code in Activity:
listUsers = mutableListOf()
adapter = WordAdapters(this, listUsers )
recyclerView.adapter = adapter
wordViewModel = ViewModelProviders.of(this,
WordViewModelFactory(this)).get(WordsViewModel::class.java)
wordViewModel!!.getData().observe(this, { t: ArrayList<sf>? ->
listUsers.clear()
t?.let { listUsers.addAll(it)
}
adapter.notifyDataSetChanged()
})
ViewModel:
class WordsViewModel ( context: Context) : ViewModel() {
private var listData = MutableLiveData<ArrayList<sf>>()
init {
val wordRepository: WordsRepository by lazy {
WordsRepository
}
//if (context.isInternetAvailable()) {
listData = wordRepository.getMutableLiveData(context)
// }
}
fun getData(): MutableLiveData<ArrayList<sf>> {
return listData
} }
Repository:
object WordsRepository {
var call: Call<MutableList<sf>>? = null
fun getMutableLiveData(context: Context) : MutableLiveData<ArrayList<sf>> {
val mutableLiveData = MutableLiveData<ArrayList<sf>>()
//context.showProgressBar()
call = NetworkApiClient.apiService.getWordsMatching("HMM")
call!!.enqueue(object : Callback<MutableList<sf>> {
override fun onFailure(call: Call<MutableList<sf>>, t: Throwable) {
//hideProgressBar()
Log.e("error", t.localizedMessage.toString())
}
override fun onResponse(call: Call<MutableList<sf>>, response:
Response<MutableList<sf>>)
{
//hideProgressBar()
if (!response.isSuccessful){
Log.e("Code " , response.code().toString());
return
}
val raw: okhttp3.Response = response.raw()
val usersResponse : MutableList<sf>? = response.body()
/* if (usersResponse != null) {
for( movie in usersResponse[0].lfs!!){
Log.v("MainActivity", movie.vars.toString())
}
}*/
Log.e("Output : ", usersResponse.toString())
usersResponse?.let { mutableLiveData.value = it as ArrayList<sf> }
}
})
return mutableLiveData
}
}
this is the base structure of JSON: here "sf" is a string, lfs is the array, according to this JSON response link provided I get 8 lfs arrays, but currently after parsing the recyclecount is 1 which is the same in the adapter itemcount method, so I get one row displayed in recylerview and rest are ignored.
JSON response:
[
{
"sf":"HMM",
"lfs":[
{
"lf":"heavy meromyosin",
"freq":267,
"since":1971,
"vars":[
{
"lf":"heavy meromyosin",
"freq":244,
"since":1971
},
{
"lf":"Heavy meromyosin",
"freq":12,
"since":1975
},
{
"lf":"H-meromyosin",
"freq":5,
"since":1975
},
{
"lf":"heavy-meromyosin",
"freq":4,
"since":1977
},
{
"lf":"heavy meromyosin",
"freq":1,
"since":1976
},
{
"lf":"H-Meromyosin",
"freq":1,
"since":1976
}
]
},
I want to ignore "sf" string after response and parse the ArrayList which is present under the "sf" which is "lfs", so based on "lfs" I need to display the data.
Mutable live data is not accepting any other type other than sf, since I placed the observer on it.
On the json you posted, there is only one parent item ( one sf ), but you are actually trying to pass the 8 lfs children. You have to perform such transformation somewhere, it could be on the network call directly, like this:
usersResponse?.let { mutableLiveData.value = it[0].lfs as ArrayList }
Take into account two things:
It could be better to check if "it" is not empty before going for the first item.
This only works if you will always have only one item on the parent array ( this sounds strange since if this is the case then the service should be returning an object, not a list, as the root of the json. If you will receive more than one object you will have to map the response into a single list of lfs. Something like (pseudo code since I'm from my phone):
It.map( item -> item.lfs)

JSONArray into Kotling Array/List and Setting value in spinner

I am performing a HTTP request from an API that gives me a JSON Array, I have been able to parse it but I am not able to convert it into a list or array in order to set it within the spinner. Here is what I have tried:
HTTP CALL:
private fun initiate() {
var product = 1
val httpAsync = "https://link_here"
.httpGet()
.responseString { request, response, result ->
when (result) {
is Result.Failure -> {
val ex = result.getException()
println(ex)
}
is Result.Success -> {
val data = result.get()
val json = JSONArray(data)
println(json)
nebulae = listOf(json)
}
}
}
httpAsync.join()
}
the response looks like this:
[{"_nebulae_id":"2","_ss_id":"1","_product_id":"2","product_type":"Dust"},{"_nebulae_id":"2","_ss_id":"3","_product_id":"1","product_type":"Star"}]
Here is the spinner code:
val spinnerProducts: Spinner = findViewById(R.id.spinnerProducts)
var adapter= ArrayAdapter(this,android.R.layout.simple_list_item_1,nebulae)
ArrayAdapter(
this,
android.R.layout.simple_spinner_item,
nebulae // insert list
).also { adapter ->
adapter.setDropDownViewResource(android.R.layout.simple_spinner_dropdown_item)
spinnerProducts.adapter = adapter
}
P.S I am new to Kotlin and it's been very long time since I used Java.
Firstly, nebulae = listOf(json) will not parse the list inside the json, will only put the whole json as an element of a list.
Create your data class for holding response:
data class Nebulae(
#SerializedName("_nebulae_id") val nebulae_id: String,
#SerializedName("_ss_id") val ss_id: String,
#SerializedName("_product_id") val product_id: String
#SerializedName("product_type") val product_type: String
)
If you want to use Gson, then convert your response like that:
val type = object : TypeToken<List<Nebulae>>() {}.type
val decodedResponse = Gson().fromJson<List<Nebulae>>(json, type)
If you want to use kotlinx.serialization, then convert your response as follows:
val decodedResponse = Json.decodeFromString<List<Nebulae>>(your_json_string)

How To Filter Retrofit2 Json Response To Keep Only Certain Elements In ArrayList

I'm new to kotlin so this maybe a very easy issue to resolve.
What I'm trying to do is filter the json response that I receive using Retrofit2 before I display the images in a grid with a RecyclerView.
instagram.com/explore/tags/{hashtag}/?__a=1&max_id= Using Retrofit2 I'm able to get the data response fine and also display the given url images in a RecyclerView.
I have not been successful in using the filter, map, loops and conditions to remove elements from the Arraylist. I do not understand these to the fullest extent but I have searched looking for solutions and those are what I came apon.
Interface
interface InstagramDataFetcher
{
#GET("tags/{tag}/?__a=1&max_id=")
fun getInstagramData(#Path("tag") hashtag: String) : Call <InstagramResponse>
}
Where I get my response from and also get StringIndexOutOfBoundsException
class InstagramFeedFragment : Fragment()
{
private fun onResponse()
{
val service = RestAPI.retrofitInstance?.create(InstagramDataFetcher::class.java)
val call = service?.getInstagramData("hashtag")
call?.enqueue(object : Callback<InstagramResponse>
{
override fun onFailure(call: Call<InstagramResponse>, t: Throwable)
{
Log.d("FEED", " $t")
}
override fun onResponse(
call: Call<InstagramResponse>, response: Response<InstagramResponse>
)
{
//for ((index, value) in data.withIndex())
if (response.isSuccessful)
{
var data: ArrayList<InstagramResponse.InstagramEdgesResponse>? = null
val body = response.body()
data = body!!.graphql.hashtag.edge_hashtag_to_media.edges
for ((index, value) in data.withIndex())
{
if(value.node.accessibility_caption[index].toString().contains("text") ||
value.node.accessibility_caption[index].toString().contains("person"))
{
data.drop(index)
}
}
recyclerView.adapter = InstagramGridAdapter(data, parentFragment!!.context!!)
}
}
})
}
}
This is my model class
data class InstagramResponse(val graphql: InstagramGraphqlResponse)
{
data class InstagramGraphqlResponse(val hashtag: InstagramHashtagResponse)
data class InstagramHashtagResponse(val edge_hashtag_to_media: InstagramHashtagToMediaResponse)
data class InstagramHashtagToMediaResponse(
val page_info: InstagramPageInfo,
val edges: ArrayList<InstagramEdgesResponse>
)
data class InstagramPageInfo(
val has_next_page: Boolean,
val end_cursor: String
)
data class InstagramEdgesResponse(val node: InstagramNodeResponse)
data class InstagramNodeResponse(
val __typename: String,
val shortcode: String,
val display_url: String,
val thumbnail_src: String,
val thumbnail_resources: ArrayList<InstagramThumbnailResourceResponse>,
val is_video: Boolean,
val accessibility_caption: String
)
data class InstagramThumbnailResourceResponse(
val src: String,
val config_width: Int,
val config_height: Int
)
}
Simply again, I want to just remove elements from the arraylist that match certain things what I don't want. For instance. the "is_video" value that comes from the json. I want to go through the arraylist and remove all elements that have "is_video" as true.
Thanks
If you asking how to filter the list then below is the demo.
You just need to use filter on your data which is an ArrayList. I've tried keeping the same structure for the models so that you can get a better understanding.
fun main() {
val first = InstagramNodeResponse(
title = "first",
is_video = true
)
val second = InstagramNodeResponse(
title = "second",
is_video = false
)
val list: ArrayList<InstagramEdgesResponse> = arrayListOf(
InstagramEdgesResponse(node = first),
InstagramEdgesResponse(node = second)
)
val itemsWithVideo = list.filter { it.node.is_video == true }
val itemsWithoutVideo = list.filter { it.node.is_video == false }
println(itemsWithVideo.map { it.node.title }) // [first]
println(itemsWithoutVideo.map { it.node.title }) // [second]
}
// Models
data class InstagramEdgesResponse(val node: InstagramNodeResponse)
data class InstagramNodeResponse(
val title: String,
val is_video: Boolean
)

How to copy a property between 2 lists of different types using declarative Kotlin?

Context
Using a declarative approach in Kotlin, need to copy a single name property from List of User objects to a List of UserDetail objects based on matching id properties as shown below:
val users = Arrays.asList(
User(1, "a"),
User(2, "b")
)
val details = Arrays.asList(
UserDetail(1),
UserDetail(2)
)
val detailsWithName = copyNameToUser(users, details)
Models are:
class User {
var id = -1;
var name = "" // given for all Users
constructor(id: Int, name: String)
// ...
}
class UserDetail {
var id = -1;
var name = "" // blank for all UserDetails
constructor(id: Int)
// ...
}
Problem
Tried to use a declarative approach via forEach iterable function:
fun copyNameToDetails(users: List<User>, details: List<UserDetail>): List<UserDetail> {
details.forEach(d ->
users.forEach(u ->
if (d.id == u.id) {
d.name = u.name
}
)
)
return details
}
This can be achieved in Java as shown below:
private static List<UserDetail> copyNameToDetails(List<User> users, List<UserDetail> details) {
for (UserDetail d: details) {
for (User u : users) {
if (d.id == u.id) {
d.name = u.name;
}
}
}
return details;
}
Question
How can this be done in Kotlin using a declarative approach?
You make too many iterations over both lists (users.size * details.size) so creating a hashmap can fix it a bit:
fun copyNameToUsers(users: List<User>, details: List<UserDetail>): List<UserDetail> {
val usersById = users.associate { it.id to it }
details.forEach { d ->
usersById[d.id]?.let { d.name = it.name }
}
return details
}
An other approach with non mutable values :
data class User(val id: Int = -1, val name: String = "")
data class UserDetail(val id: Int = -1, val name: String = "")
private fun List<UserDetail>.copyNameToUser(users: List<User>): List<UserDetail> = map { userDetail ->
users.firstOrNull { userDetail.id == it.id }?.let { userDetail.copy(name = it.name) } ?: userDetail
}

Serialized Data Class combined with built-in modifications

I am working on updating the parsing of an API response that uses a Serialized Data Class to parse the JSON response. The serialization works perfectly fine right now, but the new data that I'm attempting to parse into data class is not fully reliant on data in the json. Here is what I mean by that:
The data class is Career, and the new data I need to parse is a set of skills and each have a rating. The json data is very simple and contains the skills as such:
{
// other career data
...
"mathematics_skill": 8,
"critical_thinking_skill": 6
... // the remaining skills
}
Using straight serialization, I would only be able to store the data as such:
data class Career(
// Other career data
#serializableName("mathematic_skill") val mathSkill: Int,
#serializableName("critical_thinking_skill") val mathSkill: Int,
// remaining skills
)
However, I would like to store all skills in an array variable of a custom skills class that not only contains the rating, but also the name of the skill and a color. Basically, when I access the skills data of a career, I would like to access it like such:
val careerMathSkill = career.skills[0]
val mathRating = careerMathSkill.rating
val mathColor = careerMathSkill.color
Is it possible to use the serialized data from the data class to add non-serialized data to the same data class? (Sorry for the weird wording, not sure how else to explain it)
EDIT: Here is what I have:
class CareersRemote(
#SerializedName("careers") val careers: List<Career>
) {
companion object {
fun parseResponse(response: Response<CareersRemote>): CareersResponse {
return if (response.isSuccessful) {
response.body()!!.format()
} else
CareersResponse(listOf(CareersResponse.ErrorType.Generic()))
}
}
fun format(): CareersResponse {
val careers = topCareers.map {
Career(
id = it.id,
title = it.title,
)
}.toMutableList()
return CareersResponse(CareersResponse.SuccessData(careers = careers))
}
data class Career(
#SerializedName("id") val id: String,
#SerializedName("title") val title: String,
)
}
Here is what I am hoping to do in a way
class CareersRemote(
#SerializedName("careers") val careers: List<Career>
) {
companion object {
fun parseResponse(response: Response<CareersRemote>): CareersResponse {
return if (response.isSuccessful) {
response.body()!!.format()
} else
CareersResponse(listOf(CareersResponse.ErrorType.Generic()))
}
}
fun format(): CareersResponse {
val careers = topCareers.map {
Career(
id = it.id,
title = it.title,
)
}.toMutableList()
return CareersResponse(CareersResponse.SuccessData(careers = careers))
}
data class Career(
#SerializedName("id") val id: String,
#SerializedName("title") val title: String,
// skills array that will need to be filled out based on the data I got in the json
var skills: List<Skill>
)
}
EDIT: The suggested solution
class CareersRemote(
#SerializedName("careers") val careers: List<Career>
) {
companion object {
fun parseResponse(response: Response<CareersRemote>): CareersResponse {
return if (response.isSuccessful) {
response.body()!!.format()
} else
CareersResponse(listOf(CareersResponse.ErrorType.Generic()))
}
}
fun format(): CareersResponse {
val careers = topCareers.map {
Career(
id = it.id,
title = it.title,
)
}.toMutableList()
return CareersResponse(CareersResponse.SuccessData(careers = careers))
}
data class Career(
#SerializedName("id") val id: String,
#SerializedName("title") val title: String,
#SerializedName("math_skill") val mathSkill: Int
#SerializedName("other_skill") val mathSkill: Int
) {
var skills: List<Skill> = {
val mathSkill = Skill(name: "Math", rating: mathSkill, color: /**some color*/)
val otherSkill = Skill(name: "Other", rating: otherSkill, color: /**some color*/)
return listOf(mathSkill, otherSkill)
}
}
}
Yes, you can create a custom JsonDeserializer to modify how the JSON is parsed.
Here is a basic example of what that would look like.
class CareerDeserializer : JsonDeserializer<Career> {
override fun deserialize(json: JsonElement, typeOfT: Type, context: JsonDeserializationContext): Career {
val obj = json.asJsonObject
// standard career data
val id = obj.get("id")?.asString
val name = obj.get("name").asString
// making a Skill object
val skill = Skill(
obj.get("mathematic_skill").asInt,
obj.get("critical_thinking_skill").asInt,
obj.get("swimming_skill").asInt
// etc
)
return Career(id, name, skill)
}
}
And make sure to register that within your GsonBuilder.
val gson = GsonBuilder()
.registerTypeAdapter(Career::class.java, CareerDeserializer())
.create()
Note, you'll also have to create a JsonSerializer if you want to go the other way too.
Edit:
However, if you're just looking to change the syntax of how you're accessing that data, you can do something like this.
data class Career(
// Other career data
val mathSkill: Int,
val thinkSkill: Int
// remaining skills
) {
val skills: List<Int>
get() = listOf(mathSkill, thinkSkill)
}
This would give you a skills list back whenever you needed it, and it would be created when you accessed it, so you won't have to worry about the data being out of sync. This would allow you to access your data as such.
career.skills[0] // get the math skill.
And you can take this another step further by adding a get operator to your Career class.
data class Career(
// Other career data
val mathSkill: Int,
val thinkSkill: Int
// remaining skills
) {
...
operator fun get(pos: Int) = skills[pos]
}
Now, you can simply do
career[0] // get the math skill.
Warning, this is dangerous because you're accessing an Array so you could get OutOfBoundsExceptions. Use constants to help you out.
Edit 2:
val skills = {
listOf(Skill("Math", mathSkill, /** some color */ ),
Skill("Other", otherSkill, /** some color */ ))
}

Categories

Resources