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
Related
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
Kotlin serialization is hard! How do I do get Kotlin to believe that the values in my properties map are either primitives or classes annotated with #Serializable?
I'm trying to turn a class like this: class Entity(val id: String, val type: String, val properties: Map<String, *>) where I know that * is primitive or #Serializable or String into JSON, e.g.: { id: "0", type: "falcon", max-flight-range-nm: 10 }
import kotlinx.serialization.*
import kotlinx.serialization.encoding.Encoder
import kotlinx.serialization.json.Json
import kotlinx.serialization.json.JsonObject
import kotlinx.serialization.json.encodeToJsonElement
fun main() {
val json = Json {
prettyPrint = true
}
val falcon = Entity("0", "falcon", mapOf("max-flight-range-nm" to 10))
println(json.encodeToString(falcon))
// Desired JSON (no nesting of 'Entity.parameters', they bump-up into parent intentionally
// { id: "0", type: "falcon", max-flight-range-nm: 10 }
// Avoiding extra nesting is conceptually similar to Jackson's #JsonAnyGetter annotation: https://www.baeldung.com/jackson-annotations#1-jsonanygetter
}
#Serializable
class Entity(val id: String, val type: String, #Contextual val properties: Map<String, *>) {
#Serializer(forClass = Entity::class)
companion object : KSerializer<Entity> {
override fun serialize(encoder: Encoder, value: Entity) {
encoder.encodeString(value.id)
encoder.encodeString(value.type)
// attempted hack to encode properties at this level
for ((mapKey, mapValue) in value.properties) {
val valueJson = Json.encodeToJsonElement(mapValue)
val jsonObject = JsonObject(mapOf(mapKey to valueJson))
encoder.encodeString(jsonObject.toString())
}
}
}
}
I have full control over all code, so if needed I could go so far as to write a bunch of sealed class PropertyValue subclasses and mark them as #Serializable - so IntPropertyValue, StringPropertyValue, FooPropertyValue, etc. Maybe that's the only way?
Unfortunately the above fails at compile-time (no errors in Intelli-J though):
org.jetbrains.kotlin.codegen.CompilationException: Back-end (JVM) Internal error: Serializer for element of type Any? has not been found.
I have full control over all code, so if needed I could go so far as to write a bunch of sealed class PropertyValue subclasses and mark them as #Serializable - so IntPropertyValue, StringPropertyValue, FooPropertyValue, etc. Maybe that's the only way?
You need to make PropertyValue itself #Serializable (see https://github.com/Kotlin/kotlinx.serialization/blob/master/docs/polymorphism.md) and of course use val properties: Map<String, PropertyValue> instead of val properties: Map<String, *> (since the error message says "Serializer for element of type Any?" you might have forgotten this step).
Also note that by default you won't get
{ id: "0", type: "falcon", max-flight-range-nm: 10 }
but e.g. (omitting key quotes)
{ id: "0", type: "falcon", properties: {
max-flight-range-nm: {
type: "your_package.IntPropertyValue", value: 10
}
} }
If you want to parse/produce that particular JSON, write a custom serializer, and then you might get away with Map<String, *> too (but that's still usually a bad idea).
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.
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'm creating my first application with AKKA-http. I'm currently using spray-json to write object to json. When I created GET request everything is working fine, but I tried a POST request and the following error shows:
Error:(55, 26) could not find implicit value for parameter um: akka.http.scaladsl.unmarshalling.FromRequestUnmarshaller[nl.quintor.model.Employee]
entity(as[Employee]) { request =>
This is my main class:
object Main extends App with Routes {
implicit val system = ActorSystem("system")
implicit val executor: ExecutionContext = system.dispatcher
implicit val materializer: ActorMaterializer = ActorMaterializer()
override val slickboard = createSlickboard
Http().bindAndHandle(routes, httpInterface, httpPort)
private def createSlickboard: ActorRef =
system.actorOf(Slickboard.props(system), applicationName)
}
}
I defined my routes in the following trait:
trait Routes extends CORSSupport with Protocols {
val slickboard: ActorRef
implicit val timeout = Timeout(5 seconds)
val routes =
corsHandler {
pathPrefix("employees") {
pathEnd {
get {
complete {
(slickboard ? GetAllEmployees).mapTo[HttpResponse]
}
}
}
} ~
pathPrefix("employee") {
path(IntNumber) { id =>
get {
complete {
(slickboard ? GetEmployeeById(id)).mapTo[HttpResponse]
}
} ~
post {
decodeRequest {
entity(as[Employee]) { request =>
complete {
request.toString()
}
}
}
}
}
}
}
}
I defined my protocol just like spray recommended:
trait Protocols extends DefaultJsonProtocol {
implicit val employeeFormat = jsonFormat8(Employee.apply)
}
The class I'm trying to convert to json is the following:
case class Employee(ID: Int, firstname: String, lastname: String, street: String, zipCode: String, city: String, phoneNumber: String, image: String)
I tried multiple solutions found in stack overflow, but none of them are actually working.
Could someone please help me with this problem?
I suspect you need
import akka.http.scaladsl.marshallers.sprayjson.SprayJsonSupport._