How to use #Serializer on deeper JSON datastructures - json

I'm new in Kotlin as a PHP dev. I have a data model, something like this:
#Serializable
data class Site (
#SerialName("id")
val id: Int,
#SerialName("name")
val name: String,
#SerialName("accountId")
val accountId: Int,
}
I have JSON output something like the following, which comes from a external API and which I am unable to control:
{
"sites": {
"count": 1,
"site": [
{
"id": 12345,
"name": "Foobar",
"accountId": 123456
}
]
}
}
When trying to get this from the API with ktor HTTPClient, I'd like to instruct the serializer to use sites.site as the root for my Site datamodel. Currently, I get the error: Uncaught Kotlin exception: io.ktor.serialization.JsonConvertException: Illegal input and Caused by: kotlinx.serialization.json.internal.JsonDecodingException: Expected start of the array '[', but had 'EOF' instead at path: $
I'm using the following to fetch the endpoint:
package com.example.myapplication.myapp
import com.example.myapplication.myapp.models.Site
import io.ktor.client.*
import io.ktor.client.call.*
import io.ktor.client.plugins.contentnegotiation.*
import io.ktor.client.request.*
import io.ktor.client.statement.*
import io.ktor.http.*
import io.ktor.serialization.kotlinx.json.*
import kotlinx.serialization.json.Json
class Api {
private val client = HttpClient {
install(ContentNegotiation) {
json(Json {
prettyPrint = true
isLenient = true
ignoreUnknownKeys = true
})
}
}
private val apiKey = "REDACTED"
private val installationId = "REDACTED"
private val apiHost = "REDACTED"
suspend fun getSitesList(): List<Site> {
return get("sites/list").body()
}
suspend fun get(endpoint: String): HttpResponse {
val response = client.get(buildEndpointUrl(endpoint))
return response
}
private fun buildEndpointUrl(endpoint: String): HttpRequestBuilder {
val builder = HttpRequestBuilder()
val parametersBuilder = ParametersBuilder()
parametersBuilder.append("api_key", apiKey)
builder.url {
protocol = URLProtocol.HTTPS
host = apiHost
encodedPath = endpoint
encodedParameters = parametersBuilder
}
builder.header("Accept", "application/json")
return builder
}
}

You have to model the whole response object and cannot just provide a model for some of its parts.
#Serializable
data class SitesResponse(
val sites: SitesContainer,
)
#Serializable
data class SitesContainer(
val count: Int,
val site: List<Site>,
)
#Serializable
data class Site(
val accountId: Int,
val id: Int,
val name: String,
)

you can try make your data model like this,
data class Site(
#SerializedName("sites")
var sites: Sites) {
data class Sites(
#SerializedName("count")
var count: Int,
#SerializedName("site")
var site: List<Site>
) {
data class Site(
#SerializedName("accountId")
var accountId: Int,
#SerializedName("id")
var id: Int,
#SerializedName("name")
var name: String
)
}}

Related

Parse Json with Kotlin

I have the following json to parse:
{
"id":"123",
"nbElements":15,
"containers":[
{
"id":"cont1",
"capacity":3
}
],
"operations":[
{
"id":"cont1_01",
"weight":3,
"containerId":"cont1"
},
{
"id":"cont1_02",
"weight": 4,
"containerId":"cont1"
}
]
}
Using quicktype, it suggests the following code:
import com.fasterxml.jackson.annotation.*
import com.fasterxml.jackson.core.*
import com.fasterxml.jackson.databind.*
import com.fasterxml.jackson.databind.deser.std.StdDeserializer
import com.fasterxml.jackson.databind.module.SimpleModule
import com.fasterxml.jackson.databind.node.*
import com.fasterxml.jackson.databind.ser.std.StdSerializer
import com.fasterxml.jackson.module.kotlin.*
val mapper = jacksonObjectMapper().apply {
propertyNamingStrategy = PropertyNamingStrategy.LOWER_CAMEL_CASE
setSerializationInclusion(JsonInclude.Include.NON_NULL)
}
data class Welcome (
#get:JsonProperty(required=true)#field:JsonProperty(required=true)
val id: String,
#get:JsonProperty(required=true)#field:JsonProperty(required=true)
val nbElements: Long,
#get:JsonProperty(required=true)#field:JsonProperty(required=true)
val containers: List<Container>,
#get:JsonProperty(required=true)#field:JsonProperty(required=true)
val items: List<Item>
) {
fun toJson() = mapper.writeValueAsString(this)
companion object {
fun fromJson(json: String) = mapper.readValue<Welcome>(json)
}
}
data class Container (
#get:JsonProperty(required=true)#field:JsonProperty(required=true)
val id: String,
#get:JsonProperty(required=true)#field:JsonProperty(required=true)
val capacity: Long
)
data class Item (
#get:JsonProperty(required=true)#field:JsonProperty(required=true)
val id: String,
#get:JsonProperty(required=true)#field:JsonProperty(required=true)
val weight: Long,
#get:JsonProperty("containerId", required=true)#field:JsonProperty("containerId", required=true)
val containerID: String
)
I want to store the whole container object in each item instead of the containerId but I don't want to change the json, it must be done in the code.
More generally, is there way to make the code more concise? Is there more suitable tool than jackson for example.

class com.google.gson.internal.LinkedTreeMap cannot be cast to class Partner

import com.google.gson.Gson
import com.google.gson.reflect.TypeToken
fun main() {
val jsonString: String = """{
"jsonrpc": "2.0",
"id": null,
"result": [
{
"id": 1,
"name": "Lekhnath Rijal"
},
{
"id": 2,
"name": "Administrator"
}
]
}"""
val body1 = Gson().fromJson<RpcResult<List<Partner>>>(jsonString, object: TypeToken<RpcResult<List<Partner>>>(){}.type)
println(body1.result[0].name) // prints Lekhnath Rijal // - As expected
val body2 = fromJson<RpcResult<List<Partner>>>(jsonString)
println(body2.result[0].name) // throws Exception as stated below after this code snippet
}
fun <T> fromJson(json: String?): T {
return Gson().fromJson<T>(json, object: TypeToken<T>(){}.type)
}
data class RpcResult<T>(
val jsonrpc: String,
val id: Int?,
val result: T
)
data class Partner(
val id: Int,
val name: String
)
Exception: java.lang.ClassCastException: class com.google.gson.internal.LinkedTreeMap cannot be cast to class RpcResult
while converting json string to data class object without using function it works as expected but executing same code from helper function does not work and instead throws an exception mentioned above. What am I missing here?
It is due to type erasure in runtime. In Kotlin you can solve this issue by making your function inline with reified type:
Change your function from:
fun <T> fromJson(json: String?): T {
return Gson().fromJson<T>(json, object: TypeToken<T>(){}.type)
}
To:
inline fun <reified T> fromJson(json: String?): T {
return Gson().fromJson<T>(json, object: TypeToken<T>(){}.type)
}
For further reading check this out: https://kotlinlang.org/docs/reference/inline-functions.html

Kotlin Json Parser

I have this JSON in Kotlin, and I'm not able to fetch and parse. Any quick help. Please.
[{platform: {name: "mena-web",publishingRegion: "mena",platformGroup:"web",id: 2,countryCode: "AE",locales: {locale:["en_US","ar_AE"]}}}]
Here are my data classes:
data class Locales(var locale: ArrayList<String>) {}
data class Platform(var name: String, var publishingRegion: String, var platformGroup: String, var id: Int, var countryCode: String, var locales: Locales) {}
data class Json(var platform: Platform) {}
Here is my JSON API interface:
interface Api {
#GET("/me.json2")
fun getGeo(callback: Callback<List<Json>>): Call<List<Json>>
}
Here is my RestAPI:
class RestAPI(val api: Api) {
fun getNews(callback: Callback<List<Json>>) {
val call = api.getGeo(callback)
call.enqueue(callback)
}
}
Here is my RestAPI call:
try {
val api: RestAPI
val retrofit = Retrofit.Builder()
.baseUrl(PLATEFORM_URL)
.addConverterFactory(MoshiConverterFactory.create())
.build()
api = retrofit.create(RestApi::class.java)
val callback = object : Callback<List<Json>> {
override fun onResponse(call: Call<List<Json>>?, response: retrofit2.Response<List<Json>>?) {
response?.isSuccessful.let {
this#MainActivity.photos = response?.body()
}
}
override fun onFailure(call: Call<List<Json>>?, t: Throwable?) {
Log.e("MainActivity", "Problems calling API", t)
}
}
api.getGeo(callback)
// Log.e("Message", test.getNews().toList().toString())
} catch(e:Exception){
Log.e("Message", e.message)
}
Thanks Guys!
I found the answer, everything stat working after changing the parser
MoshiConverterFactory to GsonConverterFactory.
respones is the string which will be your jsonResponse
try {
val rootArray = JSONArray(respones)
val mainObject=rootArray.getJSONObject(0)
val platformObject=mainObject.getJSONObject("platform")
val name=platformObject.getString("name")
val publishingRegion=platformObject.getString("publishingRegion")
val platformGroup=platformObject.getString("platformGroup")
val id=platformObject.getInt("id")
val countryCode=platformObject.getString("countryCode")
val localesObj=platformObject.getJSONObject("locales")
val localeArray=locales.getJSONArray("locale")
val stringOne=localeArray.getString(0)
val stringTwo=localeArray.getString(1)
} catch (e: JSONException){}

How to edit existing JSON object with sprayJSON

I am using akka with spray json support for which I need to edit value in the recieved json.
import akka.http.scaladsl.server.Directives
import akka.http.scaladsl.marshallers.sprayjson.SprayJsonSupport
import spray.json._
final case class Item(name: String, id: Long)
final case class Order(items: List[Item],orderTag:String)
trait JsonSupport extends SprayJsonSupport with DefaultJsonProtocol {
implicit val itemFormat = jsonFormat2(Item)
implicit val orderFormat = jsonFormat2(Order)
}
In my use case I recieve the json with orderTag value as null, all I need to do is edit the orderTag value with and then use it as entity value.Is it possible to write/edit jsonObject and How to do that ?
class MyJsonService extends Directives with JsonSupport {
// format: OFF
val route =
get {
pathSingleSlash {
complete(Item("thing", 42)) // will render as JSON
}
} ~
post {
entity(as[Order]) { order => // will unmarshal JSON to Order
val itemsCount = order.items.size
val itemNames = order.items.map(_.name).mkString(", ")
complete(s"Ordered $itemsCount items: $itemNames")
}
}
}
You can just edit the json AST like ..
val json = """{"orderTag":null}"""
val jsVal = json.parseJson
val updatedJs = if (jsObj.fields.get("orderTag") == Some(JsNull)) {
JsObject(jsObj.fields + ("orderTag" -> JsString("new tag")))
} else {
jsObj
}
updatedJs.compactPrint
res26: String = """
{"orderTag":"new tag"}
"""

How to implement implicit Json Writes of Future object in Play Framework 2.x

I am new with play framework and i want to ask periodically to amazon about some products in order to insert them into a kafka topic, an error happens when i try to compile the code.
This is the code of the KafkaProducer:
file example.model.AmazonProducerExample
//UPDATED method with the suggestions from the users, thank you guys!
package example.utils
import jodd.lagarto.dom.{NodeSelector, LagartoDOMBuilder}
import example.model.AmazonProduct
import scala.collection.JavaConversions._
import scala.concurrent.ExecutionContext.Implicits.global
import scala.concurrent.Future
import play.api.libs.json._
import example.utils._
import example.producer._
object AmazonPageParser {
private val topicName = "amazonRatingsTopic"
private val producer = Producer[String](topicName)
def parse(productId: String): Future[AmazonProduct] = {
val url = s"http://www.amazon.com/dp/$productId"
HttpClient.fetchUrl(url) map {
httpResponse =>
if (httpResponse.getStatusCode == 200) {
val body = httpResponse.getResponseBody
val domBuilder = new LagartoDOMBuilder()
val doc = domBuilder.parse(body)
val responseUrl = httpResponse.getUri.toString
val nodeSelector = new NodeSelector(doc)
val title = nodeSelector.select("span#productTitle").head.getTextContent
val img = nodeSelector.select("div#main-image-container img").head.getAttribute("src")
val description = nodeSelector.select("div#feature-bullets").headOption.map(_.getHtml).mkString
val amazonProduct = AmazonProduct(productId, title, responseUrl, img, description)
println("amazonProduct is " + amazonProduct.toString)
amazonProduct
} else {
println("An error happened!")
throw new RuntimeException(s"Invalid url $url")
}
}//map
}//parse method
def main(args: Array[String]): Unit = {
//Scala Puzzlers...
AmazonPageParser.parse("0981531679").onSuccess { case amazonProduct =>
implicit val amazonFormat = Json.format[AmazonProduct]
producer.send(Json.toJson(amazonProduct).toString)
println("amazon product sent to kafka cluster..." + amazonProduct.toString)
}
}
}
file example.model.Models
package example.model
import play.api.libs.json.Json
import reactivemongo.bson.Macros
case class AmazonProduct(itemId: String, title: String, url: String, img: String, description: String)
case class AmazonRating(userId: String, productId: String, rating: Double)
case class AmazonProductAndRating(product: AmazonProduct, rating: AmazonRating)
// For MongoDB
object AmazonRating {
implicit val amazonRatingHandler = Macros.handler[AmazonRating]
implicit val amazonRatingFormat = Json.format[AmazonRating]
}
file example.utils.AmazonPageParser
The compiler returns me this error:
[error] /Users/aironman/my-recommendation-spark-engine/src/main/scala/example/producer/AmazonProducerExample.scala:25: No Json serializer found for type scala.concurrent.Future[example.model.AmazonProduct]. Try to implement an implicit Writes or Format for this type.
[error] producer.send(Json.toJson(amazonProduct).toString)
[error] ^
I have readed this post with most votes but it does not work for me.
Can anybody help me?
Writes[T] produces Json. You can't produce it directly from Future without blocking.
However you can add "callback" to this future, like this:
amazonPageParser.parse(productId).onSuccess { case amazonProduct =>
producer.send(Json.toJson(amazonProduct).toString)
}
Or with other Future methods, like map or foreach.