Android Kotlin Json Data parsing MVVM - json

I am new to android and currently I am learning kotlin I am using MVVM to get the remote data using Retrofit but I am not sure how to parse or implement the data got from JSON I am using json converter plugin to convert the json to data class. Below are my implementation i am not sure how to call or get the DATA from MyPostItems
This is the link I am using to get the JSON -> https://jsonplaceholder.typicode.com/posts -> As the arraylist does not have any name
I have two Data Class generated by JSON Coverter
class MyPost : ArrayList<MyPostItem>() -> you see here it does not even say Data Class and this is the base class i am calling using retrofit
#Entity(tableName = "Posts")
data class MyPostItem(
val body: String,
#PrimaryKey(autoGenerate = true)
val id: Int? = null,
val title: String,
val userId: Int
)
Here is my MVVM call from main Activity
viewModel.postData.observe(this, Observer { response->
when(response) {
is NetworkResource.Success -> {
binding.mainActivityProgressBar.visibility = View.GONE
response.data?.let { postResponse->
postAdapter.differ.submitList(postResponse) -> here i am getting the response but its the whole list not in the form of MyPostItems
}
}
is NetworkResource.Error -> {
binding.mainActivityProgressBar.visibility = View.GONE
response.message?.let { message ->
Log.e(TAG, "An error occured: $message")
}
}
is NetworkResource.Loading -> {
binding.mainActivityProgressBar.visibility = View.VISIBLE
}
}
})
}
P.S If anyone has resources or any study material on how to work with nested JSON object the link or reference to that would be very much apricated

Related

Deserializing a json object property to a String using kotlinx.serialization

Given json as follows where the structure of the payload object will vary:
{
"id": 1,
"displayName": "Success",
"payload": {
"someProperty": "example",
"someOtherProperty": {
"someNestedProperty": "example"
}
}
}
...using kotlinx.serialization how can I deserialize this into the following data class, where the value of payload should be the raw json string of the payload object.
#Serializable
data class Stub(
val id: Int,
val displayName: String,
val payload: String
)
Struggled to find a way of doing this with Serializers, but it was simple enough to implement manually using JsonElement.
val jsonObject = Json.parseToJsonElement(jsonString).jsonObject
val stub = Stub(
jsonObject["id"]!!.jsonPrimitive.int,
jsonObject["displayName"]!!.jsonPrimitive.content,
jsonObject["payload"]!!.toString()
)
There is a way to handle using JSONTransformingSerializer. It allows you to transform the json prior to deserialization. In this case from a jsonElement into a jsonPrimitive (of type String).
First create a transformer as follows:
object JsonAsStringSerializer: JsonTransformingSerializer<String>(tSerializer = String.serializer()) {
override fun transformDeserialize(element: JsonElement): JsonElement {
return JsonPrimitive(value = element.toString())
}
}
Now apply this transfer to the specific element in your data class by adding...
#Serializable(with = JsonAsStringSerializer::class)
just above the property you want to transform. Like this...
#Serializable
data class Stub(
val id: Int,
val displayName: String,
#Serializable(with = JsonAsStringSerializer::class)
val payload: String
)
The value of payload will be a string:
"{'someProperty': 'example','someOtherProperty': {'someNestedProperty':'example'}"
If you are later trying to deserialize this into different models depending on the structure, check out the JsonContentPolymorphicSerializer feature.

Decoding json from google firebase rest api using Dart, not Flutter

I can retrieve a list of documents from a collection in a Cloud Firestore instance, in Firebase. The response contains the most verbose json I have ever seen. Here is a taste, ...
{
documents: [
{
name: projects/myprojectId/databases/(default)/documents/mycollection/0HC2spBFxEMNUc8VQLFg,
fields: {
name: {
stringValue: Jim's Bait Shop},
taxId: {
stringValue:
},
mailingAddress: {
mapValue: {
fields: {
streetAddress1: {
stringValue:
}
},
streetAddress2: {
stringValue:
},
state: {
stringValue: NC
},
city: {
stringValue: Boone
},
zipCode: {
stringValue:
}
}
}
}
},
createTime: 2020-08-31T19
:
54: 28.643464Z,
updateTime: 2020-09-01T02
:
35: 08.203028Z
},
{ ...
When trying to use jsonDecode, in dart:convert, it fails to de-serialize the json response into a collection of Dart objects.
'_InternalLinkedHashMap<String, dynamic>' is not a subtype of type 'String'
And if I use cUrl instead of Dart, the json response looks just as verbose.
I'm using the FirebaseClient in "package:firebase/firebase_io.dart" to authenticate and read the collection.
I tried to build a "reviver" function but jsonDecode would not accept it so I'm not sure how I messed that up.
Anyway, I'm not seeing much guidance in the documentation on how to marshal this verbose json response into Dart objects. I suspect this server-side Dart is somewhat new territory. I want to avoid packages that require Flutter because I'm using a prebuilt docker image, with the Dart runtime preinstalled, on Google Cloud Run. (Truthfully, I've already tried a few Flutter packages for Firestore and a Flutter docker image.) I'll take any suggestions you have.
Below is the file I've been using for testing.
import 'package:firebase/firebase_io.dart';
import 'credentials.dart'; // borrowed from a SO post
import 'dart:convert';
const base = 'https://firestore.googleapis.com/v1/projects/';
void main() async {
// get private key...
final credential = await Credentials.fetch(); // string
final fbClient = FirebaseClient(credential);
final path = base + 'my_project_id/databases/(default)/documents/my_collection'
'?mask.fieldPaths=name&mask.fieldPaths=taxId&mask.fieldPaths=mailingAddress&orderBy=orgId';
final response = await fbClient.get(path);
print(response);
final orgs = jsonDecode(response); // unhandled exception
fbClient.close();
}
I think I might need to switch to a more sophisticated json deserializer package, and annotate my model classes to explicitly map this gnarly json to specific Dart class properties. But I have not yet seen a Dart package that supports such capabilities.
I have tried to use "json_serializable: 3.4.1" but failed to get code generation to work.
An online json validator is saying the response is malformed due to an apostrophe but can I trust that? Doubt I can escape special chars.
The error message says that response is not a String, it's a Map.
That means that Firebase has already parsed the JSON for you and returns the parsed structure.
You don't need to use jsonDecode, just final orgs = response;.
The solution was to stop using FirebaseClient, because it was not wrapping the name-value pairs in double quotation marks. Just use normal http instead.
import 'package:http/http.dart' as http;
const base = 'https://firestore.googleapis.com/v1/projects/';
void main() async {
//
final uri = base + 'myproject/databases/(default)/documents/mycollection' +
'?mask.fieldPaths=name&mask.fieldPaths=taxId&mask.fieldPaths=mailingAddress&orderBy=orgId&alt=json';
var response = await http.get(uri);
// print(response.body);
var json = jsonDecode(response.body)['documents'] as List;
List l = json.map((o) => MyModelClass.fromJson(o)).toList();
https://pub.dev/packages/http

Same SerializedName for two different data types sometimes, but with Kotlin

The JSON file I'm pulling from unfortunately has a node with the same variable name but could have two different data types randomly. When I make a network call (using gson) I get the error:
com.google.gson.JsonSyntaxException: java.lang.IllegalStateException: Expected a BEGIN_ARRAY but was int at line 1 column 5344 path $[1].medium
the JSON looks like
{
"title": "Live JSON generator",
"url": google.com,
"medium": ["chicken", "radio", "room"]
}
//However sometimes medium can be:
"medium": 259
My Serialized class looks like:
data class SearchItem(
#SerializedName("title") var title: String,
#SerializedName("url") var urlStr: String,
#SerializedName("medium") val medium: List<String>? = null
) : Serializable {}
The way I'm making the network call is like this:
private val api: P1Api
fun onItemClicked(searchItem: SearchItem) {
api.getCollections { response, error ->
response.toString()
val searchItems: List<SearchItem> = Util.gson?.fromJson<List<SearchItem>>(
response.get("results").toString()
, object : TypeToken<List<SearchItem>>() {}.type)?.toList()!!
...
doStuffWithSearchItems(searchItems)
}
How do I handle both cases where "medium" can either be an array of strings or it could be an Int?
You could write custom JsonDeserializer for this case:
class SearchItemCustomDeserializer: JsonDeserializer<SearchItem> {
override fun deserialize(json: JsonElement, typeOfT: Type, context: JsonDeserializationContext): SearchItem {
val obj = json.asJsonObject
val title = obj.get("title").asString
val url = obj.get("url").asString
val mediumProp = obj.get("medium")
val medium = if(mediumProp.isJsonArray) {
mediumProp.asJsonArray.map { it.asString }
} else {
listOf(mediumProp.asString)
}
return SearchItem(
title = title,
urlStr = url,
medium = medium
)
}
}
With this class you "manually" deserialize json to object. For medium property we check is this array or simple json primitive with function mediumProp.isJsonArray. And if answer is yes - then deserialize field as json array of strings mediumProp.asJsonArray.map { it.asString } Else deserialize the field as string.
And then we register our custom SearchItemCustomDeserializer on GsonBuilder using method registerTypeAdapter
val gson = GsonBuilder()
.registerTypeAdapter(SearchItem::class.java, SearchItemCustomDeserializer())
.create()
And after this you can use this gson instance to deserialize yours objects

Receive Map in ktor server using kotlinx.serialization

I have a Ktor server running with kotlinx.serialization as it's json (de)serializer
I send my Ktor a message like this one:
{
"Brick(partNumber=004229, partName=Sticker Sheet for Set 295-1, dataSource=Rebrickable, brickImage=null, categoryID=58)": 5
}
which is a pair of an Int and this class:
import kotlinx.serialization.Serializable
#Serializable
data class Brick(
val partNumber: String,
val partName: String,
val dataSource: String,
val brickImage: String?,
val categoryID: Int?
)
But I get this error
kotlinx.serialization.json.JsonDecodingException: Invalid JSON at 0: Expected '[, kind: MAP'
at kotlinx.serialization.json.internal.JsonReader.fail(JsonReader.kt:293)
Which to me means that kotlinx.serialization expects a different syntax for the map class. Which is odd to me. When I change the type into List> it throws the same exception but with LIST instead of MAP.
EDIT: Upon further inspection, it expects a [ instead of a { at the start of the line.
My (partial) application implementation
fun Application.module(testing: Boolean = false) {
install(ContentNegotiation) { serialization() }
routing {
route("user") {
route("brick") {
post {
call.request.queryParameters["userName"]
?.let { userRepository.login(it) } // Someone else is still working login nvm this
?.let { user ->
val bricks = call.receive<Map<Brick, Int>>() // This throws an error
userRepository.addBricks(user, bricks)
call.respond(HttpStatusCode.OK)
}
?: call.respond(HttpStatusCode.Unauthorized)
}
}
}
}
}
The android retrofit function that sends the class (using GSON):
#POST("/user/brick")
suspend fun setBricksAmounts(
#Query("userName")
userName: String,
#Body
brickAmounts: Map<Brick, Int>
)
I think using a class as a key does not work in kotlinx serialization
and it looks like that class is just serialized into a string to use it as key
Instead you can receive it as Map<String, Int>
and afterwards run
bricks.mapKeys { (jsonString, number) ->
Json(JsonConfiguration.Stable).parse(Brick.Serializer, jsonString)
}
or the equivalent jackson code if you want

How to deserialize JSON with dynamic object?

I have a simple json, but the containing field has dynamic object. For instance, json can look like
{
"fixedField1": "value1",
"dynamicField1": {
"f1": "abc",
"f2": 123
}
}
or
{
"fixedField1": "value2",
"dynamicField1": {
"g1": "abc",
"g2": { "h1": "valueh1"}
}
}
I am trying to serialize this object, but not sure how to map the dynamic field
#Serializable
data class Response(
#SerialName("fixedField1")
val fixedField: String,
#SerialName("dynamicField1")
val dynamicField: Map<String, Any> // ???? what should be the type?
)
Above code fails with following error
Backend Internal error: Exception during code generation Cause:
Back-end (JVM) Internal error: Serializer for element of type Any has
not been found.
I ran into a similar problem when I had to serialize arbitrary Map<String, Any?>
The only way I managed to do this so far was to use the JsonObject/JsonElement API and combining it with the #ImplicitReflectionSerializer
The major downside is the use of reflection which will only work properly in JVM and is not a good solution for kotlin-multiplatform.
#ImplicitReflectionSerializer
fun Map<*, *>.toJsonObject(): JsonObject = JsonObject(map {
it.key.toString() to it.value.toJsonElement()
}.toMap())
#ImplicitReflectionSerializer
fun Any?.toJsonElement(): JsonElement = when (this) {
null -> JsonNull
is Number -> JsonPrimitive(this)
is String -> JsonPrimitive(this)
is Boolean -> JsonPrimitive(this)
is Map<*, *> -> this.toJsonObject()
is Iterable<*> -> JsonArray(this.map { it.toJsonElement() })
is Array<*> -> JsonArray(this.map { it.toJsonElement() })
else -> {
//supporting classes that declare serializers
val jsonParser = Json(JsonConfiguration.Stable)
val serializer = jsonParser.context.getContextualOrDefault(this)
jsonParser.toJson(serializer, this)
}
}
Then, to serialize you would use:
val response = mapOf(
"fixedField1" to "value1",
"dynamicField1" to mapOf (
"f1" to "abc",
"f2" to 123
)
)
val serialized = Json.stringify(JsonObjectSerializer, response.toJsonObject())
Note
This reflection based serialization is only necessary if you are constrained to use Map<String, Any?>
If you are free to use your own DSL to build the responses, then you can use the json DSL directly, which is very similar to mapOf
val response1 = json {
"fixedField1" to "value1",
"dynamicField1" to json (
"f1" to "abc",
"f2" to 123
)
}
val serialized1 = Json.stringify(JsonObjectSerializer, response1)
val response 2 = json {
"fixedField1" to "value2",
"dynamicField1" to json {
"g1" to "abc",
"g2" to json { "h1" to "valueh1"}
}
}
val serialized2 = Json.stringify(JsonObjectSerializer, response2)
If, however you are constrained to define a data type, and do serialization as well as deserialization you probably can't use the json DSL so you'll have to define a #Serializer using the above methods.
An example of such a serializer, under Apache 2 license, is here: ArbitraryMapSerializer.kt
Then you can use it on classes that have arbitrary Maps. In your example it would be:
#Serializable
data class Response(
#SerialName("fixedField1")
val fixedField: String,
#SerialName("dynamicField1")
#Serializable(with = ArbitraryMapSerializer::class)
val dynamicField: Map<String, Any>
)