Moshi crashing at float values - android

I have this model class
data class RvRange(val low: Int?, val high: Int?)
My JSON response is
{"low":60799.999999999985,"high":168800.00000000003}
I want Moshi to automatically convert those float/Double values to Int but it crashes.
is there any solution to this problem?

One possible solution would be creating your own adapter that internally converts Double to Int. Example:
object RvRangeAdapter : JsonAdapter<RvRange>() {
#FromJson
override fun fromJson(reader: JsonReader): RvRange? {
reader.beginObject()
var low: Int? = null
var high: Int? = null
while (reader.hasNext()) {
when (reader.nextName()) {
"low" -> low = reader.nextDouble().roundToInt() // or any other kind of rounding logic
"high" -> high = reader.nextDouble().roundToInt()
else -> reader.skipValue() // let's ignore unknown fields
}
}
reader.endObject()
return RvRange(low, high)
}
#ToJson
override fun toJson(writer: JsonWriter, value: RvRange?) {
writer.beginObject()
.name("low").value(value?.low)
.name("high").value(value?.high)
.endObject()
}
}
Once that's done, you'll need to register that adapter so that Moshi knows it can be used:
val moshi: Moshi = Moshi.Builder()
.add(RvRange::class.java, RvRangeAdapter)
// any other configuration here
.build()
When parsing the JSON you provided, the deserialised value will be:
RvRange(low=60800, high=168800)
Note that it doesn't really work with null values, so it would need some more tweaks if null is a valid value for "high" and/or "low".

Related

Ignore elements when certain attribute is null when mapping objects

I am using the following function to map a network object to a domain one.
Mapping function
fun getLocationLocalModel(input: LocationSearchResponse): List<Location> {
return input.locations.map { location ->
return#map location.bbox?.let {
Location(
name = location.name,
countryCode = location.countryCode,
north = location.bbox.north,
south = location.bbox.south,
west = location.bbox.west,
east = location.bbox.east
)
}
}.filterNotNull()
}
Network DTOs
data class LocationSearchResponse(
#SerializedName("geonames")
val locations: List<Location>
)
data class Location(val bbox: Bbox?, val countryCode: String, val countryName: String,
val geonameId: Int, val lat: String, val lng: String, val name: String)
Domain Model
#Parcelize
data class Location(val name: String, val countryCode: String, val north: Double, val south: Double, val east: Double, val west: Double) : Parcelable
What I want is to ignore the objects where bbox is null so they are not added to the resulting list of locations.
This function works, but there must be a better/simpler way to do this.
Any help would be appreciated.
As Animesh mentioned in the comments, simply change your statement to mapNotNull -- there's no need to use the return# syntax there:
return input.locations.mapNotNull { loc ->
loc.bbox?.let { bbox ->
Location(loc.name, loc.countryCode, bbox.north, bbox.south, bbox.west, bbox.east)
}
}
Alternatively you could filter first, then use !! to dereference:
return input.locations
.filter { it.bbox != null }
.map { loc->
val bbox = loc.bbox!!
Location(loc.name, loc.countryCode, bbox.north, bbox.south, bbox.west, bbox.east)
}
Personally the former seems more readable to me.

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
)

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

default value in kotlin data class not work when server send null for that value in moshi

I use Moshi for parse json from server. if server send null for item default value not set! but server not send that item default value set.
json:
{"percentChange": null,"change": "-2500.00","value": "130000","name": null}
data class:
#JsonClass(generateAdapter = true) data class Reference(val name:String? = "-",val value: Double,val change: Double,val percentChange: Double? = -10.0,)
but data for name and percentChange is null that should "-" for name and "-10.0" for percentChange. if server not send name and percentChange, default value work, but if send that null default value not work!
I use converter-moshi:2.4.0 and retrofit:2.4.0
This is working as intended because the null literal as a value for a key in JSON is semantically different than the absence of the key and value.
You can make a custom JsonAdapter for your use case.
#JsonClass(generateAdapter = true)
data class Reference(
#Name val name: String = "-",
val value: Double,
val change: Double,
val percentChange: Double? = -10.0
) {
#Retention(RUNTIME)
#JsonQualifier
annotation class Name
companion object {
#Name #FromJson fun fromJson(reader: JsonReader, delegate: JsonAdapter<String>): String {
if (reader.peek() == JsonReader.Token.NULL) {
reader.nextNull<Unit>()
return "-"
}
return delegate.fromJson(reader)!!
}
#ToJson fun toJson(#Name name: String): String {
return name
}
}
}
#Test fun reference() {
val moshi = Moshi.Builder()
.add(Reference)
.build()
val adapter = moshi.adapter(Reference::class.java)
val decoded = Reference("-", 130_000.toDouble(), (-2_500).toDouble(), null)
assertThat(adapter.fromJson(
"""{"percentChange": null,"change": "-2500.00","value": "130000"}"""))
.isEqualTo(decoded)
assertThat(adapter.fromJson(
"""{"percentChange": null,"change": "-2500.00","value": "130000","name": null}"""))
.isEqualTo(decoded)
}

Android Kotlin - Sum of JSON objects

I have two JSON files which are connected using foreign key/field (in my case one is Post( id, ...) and Comments ( postId, ...)). I need to display total number of comments per post (in my case it's always 5).
My data classes are as follows:
data class Posts(val userId: Int,
val id: Int,
val title: String,
val body: String)
data class Comments(val postId: Int,
val id: Int,
val name: String,
val email: String,
val body: String)
And here is the function which I use to get json data (I use okhttp for setting up the client and gson for getting the data):
private fun fetchCommentJson() {
val postId = intent.getIntExtra(POST_ID, -1)
val commentJsonData = commentsJSON
val client = OkHttpClient()
val request = Request.Builder().url(commentJsonData).build()
client.newCall(request).enqueue(object : Callback {
override fun onFailure(call: Call?, e: IOException?) {
toast("Something went wrong fetching your data")
}
override fun onResponse(call: Call?, response: Response?) {
val body = response?.body()?.string()
val gson = GsonBuilder().create()
val commentsDetail = gson.fromJson(body, Array<Comments>::class.java)
val postDetail = gson.fromJson(body, Array<Posts>::class.java)
runOnUiThread {
for (commentSizeList in 0 until commentsDetail.size) {
val listOfComments = commentsDetail[commentSizeList] // List of all comments by Index
val post = postDetail[postId - 1].id // Post Id
// If postId is equal to original post id
if (listOfComments.postId == post) {
// Print it out
println(listOfComments)
}
}
}
}
})
}
When I print out the list, it really does print out all the comments per particular post based on it's ID (since comparison it's done). However it prints out each comment separately (and setting listOfComments.size prints out 1 for each comment).
displaying list of all comments per post
My question is how to combine them all, so it displays total number of comments per post (in this case -> 5)?
So the way I would do this is like this:
val totalNumberOfCommentsForPost = commentsDetail?.filter {
it.postId == post
}?.size ?: 0
.filter only selects the comments with the same post id.
Then .size gets hows many are in the new filtered list.
And finally as a safety precaution ?: 0 if any of these functions return null then return 0.
If I understand what you need correctly, you could replace your entire for loop with this code using filter:
// Post Id, the way you've been calculating it
val post = postDetail[postId - 1].id
// a list of comments only containing the ones that have the ID you need
val filteredComments: List<Comments> = commentsDetail.filter { it.postId == post }
From here, you can print either the entire list:
println(filteredComments)
Or the size of the list:
println(filteredComments.size)

Categories

Resources