I am trying to serialize the content of a json string that can take the following format:
-723232569: {
"lat": 8.2,
"lon": -90.3,
"schedule": {
"friday": [
{
"date_arr": "friday",
"remarks": " OK",
"time_arr": "07:10",
"time_dep": "06:40",
"trans_name": "C"
}
]
}
However I am struggling with my current serializable class implementation. The top key (-723232569) will vary, it will be generated randomly from one iteration to another. I would like to extract they key and its value with the following class implementation.
#Serializable
data class TimeSlot(val date_arr: String,
val remarks: String,
val time_arr: String,
val time_dep: String,
val trans_link: String,
val trans_name: String,
val trans_tel: String,
val to_lat: String? = null,
val to_lon: String? = null)
#Serializable
data class Schedule(val monday: List<TimeSlot>,
val tuesday: List<TimeSlot>,
val wednesday: List<TimeSlot>,
val thursday: List<TimeSlot>,
val friday: List<TimeSlot>,
val saturday: List<TimeSlot>,
val sunday: List<TimeSlot>)
#Serializable
data class Stop(val lat: Double,
val lon: Double,
val schedule: Schedule)
However when executing the following code I am encountering
try {
val neww = """-723232569: {
"lat": 8.2,
"lon": -90.3,
"schedule": {
"friday": [
{
"date_arr": "friday",
"remarks": " OK",
"time_arr": "07:10",
"time_dep": "06:40",
"trans_name": "C"
}
]
}"""
val res = format.decodeFromString<Stop>(neww)
} catch (ioException: IOException) {
ioException.printStackTrace()
}
Unexpected JSON token at offset 27: Encountered an unknown key '-723232569'.
Use 'ignoreUnknownKeys = true' in 'Json {}' builder to ignore unknown keys.
Related
I'm trying to parse a JSON without results.
I've a response like following:
{
"0": {
"id": "4",
"nome": "Zero Gravity",
"id_stop": "0"
},
"1": {
"id": "540",
"nome": "First Name",
"id_stop": "111"
}
}
The problem is that it's not a list with a defined id, but the id is a progressive integer that varies according to the possible answers. How do I do proper parsing? I tried building a response like this:
here my service
#GET("{path}")
suspend fun getResponse(
#Path(value = "path", encoded = true) path: String,
#Query(value = "cmd") alias: String? = "cmd",
#Query(value = "id_where") idWhere: String,
): Response<ArrayList<APIResponse>>
Here APIResponse
data class APIResponse(
#JsonProperty("id")
val id: String?,
#JsonProperty("nome")
val nome: String?,
#JsonProperty("id_stop")
val idStop: String?,
) {
fun toDomain(): API {
return API( id, nome, idStop
)
}
}
here my repository
suspend fun getAPIResponse(from: String) : API {
val response = service.getResponse(path = basePath, idWhere = where)
return response.body()?.map {
it.toDomain()
}
}
This isn't the solution though, because I can't get a complete answer but I always have a single item with all fields null. Should I use a HashMap? how could i solve?
I have a json in a val filestring: String, like
[
[
"あ",
"",
"",
"",
0,
[
"あ\n(1)五十音図ア行第一段の仮名。後舌の広母音。\n(2)平仮名「あ」は「安」の草体。片仮名「ア」は「阿」の行書体の偏。\n"
],
0,
""
],
[
"足",
"あ",
"",
"",
0,
[
"あ 【足】\nあし。「―の音せず行かむ駒もが/万葉 3387」\n〔多く「足掻(アガ)き」「足結(アユイ)」など,複合した形で見られる〕\n"
],
1,
""
],
...
]
and want to parse it to kotlin.
I have a data class
data class TermBank (
val text: String,
val reading: String,
val tags: String,
val rules: String,
val popularity: Int,
val definition: List<String>,
val sequenceNumber: Int,
val tags2: String,
)
I have tried this code with the gson library
val obj = Gson().fromJson(fileString, Array<TermBank>::class.java)
and get the error com.google.gson.JsonSyntaxException: java.lang.IllegalStateException: Expected BEGIN_OBJECT but was BEGIN_ARRAY at line 1 column 3 path $[0]
I have also tried the kotlin serialization library
val obj = Json.decodeFromString<Array<TermBank>>(fileString)
and get the error kotlinx.serialization.json.internal.JsonDecodingException: Unexpected JSON token at offset 0: Expected start of the object '{', but had '[' instead at path: $[0] JSON input: [["あ","","","",0,["あ\n(1)五十音図ア.....
Edit:
As I understand, my problem stems from my format being
an array of [string, string, string, string, int, string array, int, string] arrays. I am unsure how to parse a json of this form.
I can also not think of how to do this with .split as each array has commas in it so .split(',') won't work
You can create your own Deserializer to convert those Arrays of String to your TermBank object:
class CustomDeserializer : JsonDeserializer<TermBank> {
override fun deserialize(json: JsonElement?, typeOfT: Type?, context: JsonDeserializationContext?): TermBank {
return if (json?.isJsonArray == true) {
val array = json.asJsonArray
TermBank(
array.get(0).asString,
array.get(1).asString,
array.get(2).asString,
array.get(3).asString,
array.get(4).asInt,
array.get(5).asString.split("\n"), // Not sure how you split here
array.get(6).asInt,
array.get(7).asString)
} else {
TermBank("", "", "", "", 0, listOf(), 0, "")
}
}
}
And you can build Gson with your Deserializer to convert your JSON into List<TermBank>:
val gson = GsonBuilder().registerTypeAdapter(TermBank::class.java, CustomDeserializer()).create()
val type = object : TypeToken<List<TermBank>>() {}.type
val target = gson.fromJson<List<TermBank>>(arrayJson, type)
println(target.toString())
You should have output like this:
[TermBank(text=あ, reading=, tags=, rules=, popularity=0, definition=[あ, (1)五十音図ア行第一段の仮名。後舌の広母音。, (2)平仮名「あ」は「安」の草体。片仮名「ア」は「阿」の行書体の偏。, ], sequenceNumber=0, tags2=), TermBank(text=足, reading=あ, tags=, rules=, popularity=0, definition=[あ 【足】, あし。「―の音せず行かむ駒もが/万葉 3387」, 〔多く「足掻(アガ)き」「足結(アユイ)」など,複合した形で見られる〕, ], sequenceNumber=1, tags2=)]
I'm stuck decoding a dynamic value from a json file with kotlin
{
"values": {
"16694990259825982nLJ": {
"id": "16694990259825982nLJ",
"createdAt": "2022-11-26T21:43:45.982Z",
"name": "Some Text",
"owner": "xxxx#xxxx.xx",
"category": "Some Text",
"description": "Some Text.",
"template_id": "Some Text",
"last_update": "2022-11-27T00:11:51.863Z",
"users": [
"xxxx#xxxx.xx"
]
}
}
}
Here's my data class :
#Serializable
data class WorkflowsTest(
#field:SerializedName("values")
val values: Map<String, Id>
)
#Serializable
data class Id(
#field:SerializedName("owner")
val owner: String? = null,
#field:SerializedName("createdAt")
val createdAt: String? = null,
#field:SerializedName("last_update")
val lastUpdate: String? = null,
#field:SerializedName("name")
val name: String? = null,
#field:SerializedName("description")
val description: String? = null,
#field:SerializedName("template_id")
val templateId: String? = null,
#field:SerializedName("id")
val id: String? = null,
#field:SerializedName("category")
val category: String? = null,
#field:SerializedName("users")
val users: List<String?>? = null
)
This is my ApiResponse data class when fetching Data from GET HTTP URL :
#Serializable
data class ApiResponse(
#Transient
val success: Boolean? = null,
val message: String? = null,
val values: WorkflowsTest? = null,
val status: Int,
#Transient
val error: Exception? = null
)
And this is my retrofit provider network from network module
#Provides
#Singleton
fun provideRetrofit(okHttpClient: OkHttpClient): Retrofit {
return Retrofit.Builder()
.baseUrl(BASE_URL)
.client(okHttpClient)
.addConverterFactory(GsonConverterFactory.create())
.build()
}
Logs that I get when I get the response back :
ApiResponse(success=null, message=null, **values=WorkflowsTest(values=null)**, status=1, error=null)
as you can see values are null no matter what I do, status = 1 means the request is 200 OK, success, error and message are transient and manipulated for snackbar messages.
Test on main function :
fun main() {
val jsonString = """
{
"values": {
"16694990259825982nLJ": {
"id": "16694990259825982nLJ",
"createdAt": "2022-11-26T21:43:45.982Z",
"name": "Some Text",
"owner": "xxxx#xxxx.xx",
"category": "Some Text",
"description": "Some Text.",
"template_id": "Some Text",
"last_update": "2022-11-27T00:11:51.863Z",
"users": [
"xxxx#xxxx.xx"
]
}
}
}"""
val jsonTest: WorkflowsTest =
Gson().fromJson(jsonString, WorkflowsTest::class.java)
println(jsonTest)
}
print result works fine :
WorkflowsTest(values={16694990259825982nLJ=Id(owner=xxxx#xxxx.xx, createdAt=2022-11-26T21:43:45.982Z, lastUpdate=2022-11-27T00:11:51.863Z, name=Some Text, description=Some Text., templateId=Some Text, id=16694990259825982nLJ, category=Some Text, users=[xxxx#xxxx.xx])})
> **UPDATE**
I solved the issue by only changing the Api response data class :
Old :
#Serializable
data class ApiResponse(
#Transient
val success: Boolean? = null,
val message: String? = null,
val values: WorkflowsTest? = null,
val status: Int,
#Transient
val error: Exception? = null
to the new one (focus on the values field) :
#Serializable
data class ApiResponse(
#Transient
val success: Boolean? = null,
val message: String? = null,
val values: Map<String, Id>? = null,
val status: Int,
#Transient
val error: Exception? = null
)
and magically it works.
I have to work with a server that sends these responses for any request:
For OK:
HTTP 200
{
"jsonrpc": "2.0",
"id": null,
"result": {
"data": {
"exp": 1635637589,
...
...
},
"success": true,
"errors": [],
"messages": []
}
}
For error:
HTTP 200 (yes, and unfortunately that can't be changed)
{
"jsonrpc": "2.0",
"id": null,
"result": {
"data": {},
"success": false,
"errors": [{
"code": 1001,
"message": "Error"
}],
"messages": []
}
}
Notice that data is a json object of a specific type when the response is OK, and a different one when the response is an error. This format is used for all the responses, meaning that data can have different child fields.
I want to use Retrofit + Moshi + RxJava, but I am struggling to find a way to deserialize the response to handle that data field using two different types. I have this model:
data class BaseResponse<T>(
#Json(name = "jsonrpc") val jsonrpc: String,
#Json(name = "id") val id: String?,
#Json(name = "result") val result: BaseResponseResult<T>
)
data class BaseResponseResult<T>(
#Json(name = "data") val data: T, // This is what I have a problem with
#Json(name = "success") val success: Boolean,
#Json(name = "errors") val errors: List<Error>
)
// This would be the data field
data class LoginResponse(
#Json(name = "user_id") val userId: Long,
...
...
...
)
// This would be the data field
data class ProfileResponse(
#Json(name = "name") val name: String,
...
...
...
)
And this would be my Retrofit interface
interface UsersApi {
#POST("api/login")
fun loginReal(#Body request: BaseRequest<LoginRequest>): Single<BaseResponse<LoginResponse>>
#POST("api/profile")
fun loginReal(#Body request: BaseRequest<ProfileRequest>): Single<BaseResponse<ProfileResponse>>
}
I thought about adding a custom deserializer to parse BaseResponse<T> and throw some exception in case the response was an error one, but I am not able to register a deserializer using generics. I have read Moshi's documentation and several posts about deserializers, but I can't get it to work with a generic. Is that possible with Moshi?
I have a nested JSON like this from Server, as you can see there is a nested data in location
{
"id": "18941862",
"name": "Pizza Maru",
"url": "https://www.zomato.com/jakarta/pizza-maru-1-thamrin?utm_source=api_basic_user&utm_medium=api&utm_campaign=v2.1",
"location": {
"address": "Grand Indonesia Mall, East Mall, Lantai 3A, Jl. M.H. Thamrin No. 1, Thamrin, Jakarta",
"locality": "Grand Indonesia Mall, Thamrin",
"city": "Jakarta",
"city_id": 74,
"latitude": "-6.1954467635",
"longitude": "106.8216102943",
"zipcode": "",
"country_id": 94,
"locality_verbose": "Grand Indonesia Mall, Thamrin, Jakarta"
},
"currency": "IDR"
}
I am using retrofit and using gson converter. usually I need to make 2 data class for something like this to map JSON into POJO. so I need to make Restaurant class and also Location class, but I need to flatten that json object into single Restaurant class, like this
data class Restaurant : {
var id: String
var name: String
var url: String
var city: String
var latitude: Double
var longitude: Double
var zipcode: String
var currency: String
}
how to do that if I am using retrofit and gson converter ?
java or kotlin are ok
This solution is a silver bullet for this problem and cannot be appreciated enough.
Take this Kotlin file first:
/**
* credits to https://github.com/Tishka17/gson-flatten for inspiration
* Author: A$CE
*/
#Retention(AnnotationRetention.RUNTIME)
#Target(AnnotationTarget.FIELD)
annotation class Flatten(val path: String)
class FlattenTypeAdapterFactory(
private val pathDelimiter: String = "."
): TypeAdapterFactory {
override fun <T: Any?> create(gson: Gson, type: TypeToken<T>): TypeAdapter<T> {
val delegateAdapter = gson.getDelegateAdapter(this, type)
val defaultAdapter = gson.getAdapter(JsonElement::class.java)
val flattenedFieldsCache = buildFlattenedFieldsCache(type.rawType)
return object: TypeAdapter<T>() {
#Throws(IOException::class)
override fun read(reader: JsonReader): T {
// if this class has no flattened fields, parse it with regular adapter
if(flattenedFieldsCache.isEmpty())
return delegateAdapter.read(reader)
// read the whole json string into a jsonElement
val rootElement = defaultAdapter.read(reader)
// if not a json object (array, string, number, etc.), parse it
if(!rootElement.isJsonObject)
return delegateAdapter.fromJsonTree(rootElement)
// it's a json object of type T, let's deal with it
val root = rootElement.asJsonObject
// parse each field
for(field in flattenedFieldsCache) {
var element: JsonElement? = root
// dive down the path to find the right element
for(node in field.path) {
// can't dive down null elements, break
if(element == null) break
// reassign element to next node down
element = when {
element.isJsonObject -> element.asJsonObject[node]
element.isJsonArray -> try {
element.asJsonArray[node.toInt()]
} catch(e: Exception) { // NumberFormatException | IndexOutOfBoundsException
null
}
else -> null
}
}
// lift deep element to root element level
root.add(field.name, element)
// this keeps nested element un-removed (i suppose for speed)
}
// now parse flattened json
return delegateAdapter.fromJsonTree(root)
}
override fun write(out: JsonWriter, value: T) {
throw UnsupportedOperationException()
}
}.nullSafe()
}
// build a cache for flattened fields's paths and names (reflection happens only here)
private fun buildFlattenedFieldsCache(root: Class<*>): Array<FlattenedField> {
// get all flattened fields of this class
var clazz: Class<*>? = root
val flattenedFields = ArrayList<Field>()
while(clazz != null) {
clazz.declaredFields.filterTo(flattenedFields) {
it.isAnnotationPresent(Flatten::class.java)
}
clazz = clazz.superclass
}
if(flattenedFields.isEmpty()) {
return emptyArray()
}
val delimiter = pathDelimiter
return Array(flattenedFields.size) { i ->
val ff = flattenedFields[i]
val a = ff.getAnnotation(Flatten::class.java)!!
val nodes = a.path.split(delimiter)
.filterNot { it.isEmpty() } // ignore multiple or trailing dots
.toTypedArray()
FlattenedField(ff.name, nodes)
}
}
private class FlattenedField(val name: String, val path: Array<String>)
}
Then add it to Gson like this:
val gson = GsonBuilder()
.registerTypeAdapterFactory(FlattenTypeAdapterFactory())
.create()
Retrofit.Builder()
.baseUrl(baseUrl)
...
.addConverterFactory(GsonConverterFactory.create(gson))
.build()
Using your example, you can get the pojo parsed like this:
// prefer constructor properties
// prefer val over var
// prefer added #SerializedName annotation even to same-name properties:
// to future proof and for easier proguard rule config
data class Restaurant(
#SerializedName("id") val id: String,
#SerializedName("name") val name: String,
#SerializedName("url") val url: String,
#Flatten("location.city") val city: String,
#Flatten("location.latitude") val latitude: Double,
#Flatten("location.longitude") val longitude: Double,
#Flatten("location.zipcode") val zipcode: String,
#SerializedName("currency") var currency: String
)
You can even write a path down an array, e.g #Flatten("friends.0.name") = get first friend's name.
For more info, visit this repo
Note, however, that I stripped down the TypeAdapter to only read/consume json objects. You can implement write() if you want to use it to write json too.
You are welcome.