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