Maps and variable key names in Kotlinx-Serialization - json

The meta is simple, but how do I model analysis for Kotlinx-Serialization?
{
"meta": {
"subject": "33306",
"interval": "weekly"
},
"analysis": {
"2021-07-20": {
"dose": "0.6410"
},
"2021-07-16": {
"dose": "0.9570"
},
"2021-07-09": {
"dose": "0.6880"
}
}
}
I have this at the moment.
#Serializable
class Observation(
#SerialName("meta")
val meta: Meta,
#SerialName("analysis")
val analysis: Map<String, Map<String, String>>
)
But I get the error.
kotlinx.serialization.json.internal.JsonDecodingException: Polymorphic serializer was not found for missing class discriminator ('null')

The solution was to model it as a suspend method in Retrofit and wrap its return type in Response.
#GET("/observation")
suspend fun getObservation(
#Query("subject")
subject: String
): Response<Observation>

Related

Custom JsonConverter with different types in toJson and fromJson

In order to read from and write data with relations to the PocketBase API I need custom fromJson and toJson methods to de/serialize it to my models:
#freezed
class MyModel with _$MyModel {
const factory MyModel({
String name,
#RelationsConverter() List<Relation>? relations,
}) = _MyModel;
factory MyModel.fromJson(Map<String, Object?> json) => _$MyModelFromJson(json);
}
#freezed
class Relation with _$Relation {
const factory Relation({
required String id,
String name,
}) = _Relation;
factory Relation.fromJson(Map<String, Object?> json) => _$RelationFromJson(json);
}
The Json data when reading a model from PocketBase with the fields "name" and "relations", which contains a list of relations to some other model might look like this (with the expand=relations query parameter set):
{
"name": "A model",
"relations": [
"abc123",
"def456"
],
"expand": {
"relations": [
{
"id": "abc123",
"name": "Relation A"
},
{
"id": "def456",
"name": "Relation B"
}
]
}
}
Before converting the data to my models, I transform the data so it looks like this and can be easily deserialized:
{
"name": "A model",
"relations": [
{
"id": "abc123",
"name": "Relation A"
},
{
"id": "def456",
"name": "Relation B"
}
]
}
When updating/creating data however, this form is not desired, I need it in this form:
{
"name": "An updated model with a new relation",
"relations": [
"abc123",
"def456",
"xyz999"
]
}
I was hoping this would be trivial with a custom converter. The T type is used as a placeholder since this is my main problem:
class RelationsConverter implements JsonConverter<Relation, T> {
const RelationsConverter();
#override
Relation fromJson(K json) => ...
#override
T toJson(Skill data) => ...
}
#freezed
class MyModel with _$MyModel {
const factory MyModel({
String name,
#RelationsConverter() List<Relation>? relations,
}) = _MyModel;
factory MyModel.fromJson(Map<String, Object?> json) => _$MyModelFromJson(json);
}
The problem here is that while jsonFrom is passed a Map, toJson returns a String:
Relation fromJson(Map<String, dynamic> json) => Relation.fromJson(json);
String toJson(Relation relation) => relation.id;
However I need to pass a specific type for both to-and fromJson to JsonConverter:
class RelationsConverter implements JsonConverter<Relation, [Map or String]>
I neither can omit fromJson (which I'd like to) because the interface forces me to implement both methods.
How can I solve this? I I could get around my custom transforming if the incoming data and solve this in a converter, this would also be nice.

How to serialize fields with varying type?

I have the following data classes to parse JSON. I can parse it easily with the decodeFromString method. However, the Info classes could contain the List<Int> type from time to time along with the Int type so that both are included in a single JSON. How can I handle this variation in serialization?
#Serializable
data class Node (#SerialName("nodeContent") val nodeContent: List<Info>)
#Serializable
data class Info (#SerialName("info") val info: Int)
p.s. The closest question to mine is this one: Kotlinx Serialization, avoid crashes on other datatype. I wonder if there are other ways?
EDIT:
An example is given below.
"nodeContent": [
{
"info": {
"name": "1",
},
},
{
"info": [
{
"name": "1"
},
{
"name": "2"
},
],
},
{
"info": {
"name": "2",
},
}
]
Here is an approach with a custom serializer similar to the link you provided. The idea is to return a list with just a single element.
// Can delete these two lines, they are only for Kotlin scripts
#file:DependsOn("org.jetbrains.kotlinx:kotlinx-serialization-json:1.2.0")
#file:CompilerOptions("-Xplugin=/snap/kotlin/current/lib/kotlinx-serialization-compiler-plugin.jar")
import kotlinx.serialization.*
import kotlinx.serialization.json.*
import kotlinx.serialization.encoding.Decoder
#Serializable
data class Node (val nodeContent: List<Info>)
#Serializable(with = InfoSerializer::class)
data class Info (val info: List<Name>)
#Serializable
data class Name (val name: Int)
#Serializer(forClass = Info::class)
object InfoSerializer : KSerializer<Info> {
override fun deserialize(decoder: Decoder): Info {
val json = ((decoder as JsonDecoder).decodeJsonElement() as JsonObject)
return Info(parseInfo(json))
}
private fun parseInfo(json: JsonObject): List<Name> {
val info = json["info"] ?: return emptyList()
return try {
listOf(Json.decodeFromString<Name>(info.toString()))
} catch (e: Exception) {
(info as JsonArray).map { Json.decodeFromString<Name>(it.toString()) }
}
}
}
Usage:
val ss2 = """
{
"nodeContent": [
{
"info":
{"name": 1}
},
{
"info": [
{"name": 1},
{"name": 2}
]
},
{
"info":
{"name": 2}
}
]
}
"""
val h = Json.decodeFromString<Node>(ss2)
println(h)
Result:
Node(nodeContent=[Info(info=[Name(name=1)]), Info(info=[Name(name=1), Name(name=2)]), Info(info=[Name(name=2)])])

Moshi parse json with different key

I was looking towards PolymorphicAdapter but all the polymorphic example I could find had a key called "type" or something similar that could be use to differentiate the class to use. However in my case I don't have such key. I'm a bit lost on how to parse such a peculiar json.
{
"infos": {
"1588318": {
"id": "1588318",
"id_user": "9701",
"profile_name": "Profile1",
"views": 100
},
"1588319": {
"id": "1588319",
"id_user": "7391",
"profile_name": "Profile2",
"views": 10
},
"1588320": false,
"1588321": {
"id": "1588321",
"deleted": true
}
}
}
data class UserInfo(val infos: Map<String, UserResult>)
sealed class UserResult {
data class UserDeleted(val id: String, val deleted: Boolean): UserResult()
data class UserInfoCard(
val id: String,
val title: String,
#Json(name = "profile_name") val profileName: String,
val views: Int
): UserResult()
}
In the end I didn't find any solution and after discussing with the API manager he said he would update with a key to determine if it's either a profile or a deleted_profile

Unable to create converter for class when using sealed class or an interface with Moshi

I am trying to parse a json data from a server.It has dynamic keys so I am trying to have like a parent class that have the shared keys and child class for each specific node. I wrote a kotlin code using retrofit and Moshi but it's not working. I tried with a sealed class and interface without success. Actually I would prefer that works with sealed class but I don't know what I am doing wrong
interface MyApi {
#GET("/...")
fun fetchMyFeed(): Call<MyResponse>
}
data class MyResponse(
val data: List<ParentResponse>
)
interface ParentResponse{
val name: String
}
data class Child1Response(
val age: String,
val kids: List<KidsResponse>,
val cars: List<CarsResponse>
)
data class Child2Response(
val job: String,
val address: List<AddressResponse>
)
fun fetchAllFeed(): List<Any>? =
try {
val response = api.fetchMyFeed().execute()
if (response.isSuccessful) {
Log.d("check",${response.body()?.data?})
null
} else null
} catch (e: IOException) {
null
} catch (e: RuntimeException) {
null
}```
and the json file is :
{
"data": [
{
"name": "string",
"job": "string",
"address": [
{
"avenue": "string",
"imageUrl": "string",
"description": "string"
}
]
},
{
"name": "string",
"age": "string",
"kids": {
"count": "string",
"working": "string"
},
"cars": [
{
"brand": "string",
"age": "string",
"imageUrl": "string"
}
]
}
]
}
Unable to create converter for class
You can make use of JsonAdapter from moshi to parse different JSON Models if you can differentiate them by foreseeing some value in the json.
for example, consider json response having two schemas,
{
"root": {
"subroot": {
"prop" : "hello",
"type" : "String"
}
}
}
(or)
{
"root": {
"subroot": {
"prop" : 100,
"type" : "Integer"
}
}
}
Here, subroot has different schemas (one containing string property and another containg a integer property) which can be identified by "type"
You can create a parent sealed class with common keys and derive few child classes with varying keys. Write a adapter to select the type of class to be used while json serialization and add that adapter to moshi builder.
Model classes:
class Response {
#Json(name = "root")
val root: Root? = null
}
class Root {
#Json(name = "subroot")
val subroot: HybridModel? = null
}
sealed class HybridModel {
#Json(name = "type")
val type: String? = null
class StringModel : HybridModel() {
#Json(name = "prop")
val prop: String? = null
}
class IntegerModel : HybridModel() {
#Json(name = "prop")
val prop: Int? = null
}
}
Few extension methods to JsonReader,
inline fun JsonReader.readObject(process: () -> Unit) {
beginObject()
while (hasNext()) {
process()
}
endObject()
}
fun JsonReader.skipNameAndValue() {
skipName()
skipValue()
}
HybridAdapter to select type of class for "subroot" key
class HybridAdapter : JsonAdapter<HybridModel>() {
#FromJson
override fun fromJson(reader: JsonReader): HybridModel {
var type: String = ""
// copy reader and foresee type
val copy = reader.peekJson()
copy.readObject {
when (copy.selectName(JsonReader.Options.of("type"))) {
0 -> {
type = copy.nextString()
}
else -> copy.skipNameAndValue()
}
}
//handle exception if type cannot be identified
if (type.isEmpty()) throw JsonDataException("missing type")
// build model based on type
val moshi = Moshi.Builder().build()
return if (type == "String")
moshi.adapter(HybridModel.StringModel::class.java).fromJson(reader)!!
else
moshi.adapter(HybridModel.IntegerModel::class.java).fromJson(reader)!!
}
#ToJson
override fun toJson(p0: JsonWriter, p1: HybridModel?) {
// serialization logic
}
}
Finally build Moshi with the HybridAdapter to serialize HybridModel,
fun printProp(response: Response?) {
val subroot = response?.root?.subroot
when (subroot) {
is HybridModel.StringModel -> println("string model: ${subroot.prop}")
is HybridModel.IntegerModel -> println("Integer model: ${subroot.prop}")
}
}
fun main() {
val jsonWithStringSubroot =
"""
{
"root": {
"subroot": {
"prop" : "hello",
"type" : "String"
}
}
}
"""
val jsonWithIntegerSubroot =
"""
{
"root": {
"subroot": {
"prop" : 1,
"type" : "Integer"
}
}
}
"""
val moshi = Moshi.Builder().add(HybridAdapter()).build()
val response1 = moshi.adapter(Response::class.java).fromJson(jsonWithStringSubroot)
printProp(response1) // contains HybridModel.StringModel
val response2 = moshi.adapter(Response::class.java).fromJson(jsonWithIntegerSubroot)
printProp(response2) // contains HybridModel.IntegerModel
}

Why am I getting Jackson ObjectMapper Parsing Error due to some issues in LinkedHashMap initiation?

I want to read this JSON using ObjectMapper:
{
"basePath": "/v1",
"models": {
"Course":{
"number": "integer",
"name": "string",
"description": "string"
},
"Department": {
"name": "string",
"code": "string"
}
}
}
I used Jackson ObjectMapper like this:
ObjectMapper mapper = new ObjectMapper();
InputStream inputStream = Input.class.getResourceAsStream("/input.json");
Input input = mapper.readValue(inputStream, Input.class);
Where Input is:
public class Input {
String basePath;
Map<String, Map<String, Map<String, String>>> models;
public String getBasePath() {
return basePath;
}
public void setBasePath(String basePath) {
this.basePath = basePath;
}
public Map<String, Map<String, Map<String, String>>> getModels() {
return models;
}
public void setModels(Map<String, Map<String, Map<String, String>>> models) {
this.models = models;
}
}
But I am getting JSON Mapping Error.
Can not instantiate value of type [map type; class java.util.LinkedHashMap, [simple type, class java.lang.String] -> [simple type, class java.lang.String]] from JSON String; no single-String constructor/factory method
What am I doing wrong here?
The provide json structure does not matches with the models map structure. You have two options for it either change in Map or change in json.
Map<String, Map<String, String>> models;
If change in json structure like below.
{
"basePath": "/v1",
"models": {
"Course":{
"Details":{
"number": "integer",
"name": "string",
"description": "string"
}
}
}
}
I think better to change in Map thing rather to change the JSON structure.