I'm trying to parse newline delimited json using retrofit and moshi. This is my GET function:
suspend fun getDeviceValuesNew(#Path("application-id") applicationId: String, #Path("device-id") deviceId: String)
: Response<List<ValueApiResponse>>
When I try to run it, I get this error:
com.squareup.moshi.JsonDataException: Expected BEGIN_ARRAY but was BEGIN_OBJECT at path $
The HTTP call returns json like this:
{
"result": {
"end_device_ids": {
"device_id": "esp32",
"application_ids": {}
},
"received_at": "2021-03-31T11:33:42.757281753Z",
"uplink_message": {
"decoded_payload": {
"brightness": 0
},
"settings": {
"data_rate": {}
},
"received_at": "2021-03-31T11:33:42.547285090Z"
}
}
}
{
"result": {
"end_device_ids": {
"device_id": "esp32",
"application_ids": {}
},
"received_at": "2021-03-31T11:18:17.745921472Z",
"uplink_message": {
"decoded_payload": {
"brightness": 0
},
"settings": {
"data_rate": {}
},
"received_at": "2021-03-31T11:18:17.538276218Z"
}
}
}
EDIT #1:
As you can see in my answer below, I managed to get a valid JSON response from the API, but still I'm struggling to parse these JSON objects to a list of Kotlin objects. How do I get Moshi to handle these newline delimited JSON objects as a list? I think the problem is that Moshi requires the objects to be wrapped inside an array to be recognised as a list. How do I do that?
This is my data class used for parsing:
#JsonClass(generateAdapter = true)
data class ValueDto(
#Json(name = "result")
val result: Result
) {
#JsonClass(generateAdapter = true)
data class Result(
#Json(name = "end_device_ids")
val endDeviceIds: EndDeviceIds,
#Json(name = "received_at")
val receivedAt: String,
#Json(name = "uplink_message")
val uplinkMessage: UplinkMessage
) {
#JsonClass(generateAdapter = true)
data class EndDeviceIds(
#Json(name = "application_ids")
val applicationIds: ApplicationIds,
#Json(name = "device_id")
val deviceId: String
) {
#JsonClass(generateAdapter = true)
class ApplicationIds(
)
}
#JsonClass(generateAdapter = true)
data class UplinkMessage(
#Json(name = "decoded_payload")
val decodedPayload: DecodedPayload,
#Json(name = "received_at")
val receivedAt: String,
#Json(name = "settings")
val settings: Settings
) {
#JsonClass(generateAdapter = true)
data class DecodedPayload(
#Json(name = "brightness")
val brightness: Int
)
#JsonClass(generateAdapter = true)
data class Settings(
#Json(name = "data_rate")
val dataRate: DataRate
) {
#JsonClass(generateAdapter = true)
class DataRate(
)
}
}
}
}
#tyczj was right, this is not valid ndjson, as in ndjson newline characters only appear after each seperate json text. The solution was to send Accept: text/event-stream with the HTTP request and now I'm getting a valid ndjson response like this:
{"result":{"end_device_ids":{"device_id":"esp32-bh1750","application_ids":{}},"received_at":"2021-04-24T10:19:04.021238048Z","uplink_message":{"decoded_payload":{"brightness":0},"settings":{"data_rate":{}},"received_at":"2021-04-24T10:19:03.809173924Z"}}}
{"result":{"end_device_ids":{"device_id":"esp32-bh1750","application_ids":{}},"received_at":"2021-04-24T10:25:22.260712161Z","uplink_message":{"decoded_payload":{"brightness":119},"settings":{"data_rate":{}},"received_at":"2021-04-24T10:25:22.046086937Z"}}}
{"result":{"end_device_ids":{"device_id":"esp32-bh1750","application_ids":{}},"received_at":"2021-04-24T10:18:58.438947740Z","uplink_message":{"decoded_payload":{"brightness":0},"settings":{"data_rate":{}},"received_at":"2021-04-24T10:18:58.228671174Z"}}}
{"result":{"end_device_ids":{"device_id":"esp32-bh1750","application_ids":{}},"received_at":"2021-04-24T10:21:10.102303310Z","uplink_message":{"decoded_payload":{"brightness":106},"settings":{"data_rate":{}},"received_at":"2021-04-24T10:21:09.893217735Z"}}}
{"result":{"end_device_ids":{"device_id":"esp32-bh1750","application_ids":{}},"received_at":"2021-04-24T10:23:16.177064041Z","uplink_message":{"decoded_payload":{"brightness":108},"settings":{"data_rate":{}},"received_at":"2021-04-24T10:23:15.967959055Z"}}}
{"result":{"end_device_ids":{"device_id":"esp32-bh1750","application_ids":{}},"received_at":"2021-04-24T10:27:28.334312076Z","uplink_message":{"decoded_payload":{"brightness":117},"settings":{"data_rate":{}},"received_at":"2021-04-24T10:27:28.126104222Z"}}}
{"result":{"end_device_ids":{"device_id":"esp32-bh1750","application_ids":{}},"received_at":"2021-04-24T10:29:34.400253264Z","uplink_message":{"decoded_payload":{"brightness":99},"settings":{"data_rate":{}},"received_at":"2021-04-24T10:29:34.190980301Z"}}}
{"result":{"end_device_ids":{"device_id":"esp32-bh1750","application_ids":{}},"received_at":"2021-04-24T10:31:40.481766225Z","uplink_message":{"decoded_payload":{"brightness":118},"settings":{"data_rate":{}},"received_at":"2021-04-24T10:31:40.270452429Z"}}}
{"result":{"end_device_ids":{"device_id":"esp32-bh1750","application_ids":{}},"received_at":"2021-04-24T10:33:46.567235913Z","uplink_message":{"decoded_payload":{"brightness":114},"settings":{"data_rate":{}},"received_at":"2021-04-24T10:33:46.357373037Z"}}}
{"result":{"end_device_ids":{"device_id":"esp32-bh1750","application_ids":{}},"received_at":"2021-04-24T10:35:52.737386496Z","uplink_message":{"decoded_payload":{"brightness":121},"settings":{"data_rate":{}},"received_at":"2021-04-24T10:35:52.426583804Z"}}}
I've added the Header via retrofit annotation:
#Headers("Accept: text/event-stream ")
Related
I need to parse data class from server request like this:
#JsonClass(generateAdapter = true)
data class User(
#Json(name = "user_name")
val userName: String,
#Json(name = "gender")
val gender: Gender?,
) {
enum class Gender {
#Json(name = "male")
MALE,
#Json(name = "female")
FEMALE,
}
}
I got this json from server:
{
"data":{
"user_name":"MyUserName",
"gender":null
}
}
But i have this error
com.squreup.moshi.JsonDataException: Expected a string but was NULL at path $.data.gender
Is there a way to fix this without changing server api?
UPD: The problem was in EnumJsonAdapter with unknown fallback:
Moshi.Builder()
.add(KotlinJsonAdapterFactory())
.add(
User.Gender::class.java,
EnumJsonAdapter.create(User.Gender::class.java)
.withUnknownFallback(null)
)
.build()
Solution: add nullSafe() to adapter
Moshi.Builder()
.add(KotlinJsonAdapterFactory())
.add(
User.Gender::class.java,
EnumJsonAdapter.create(User.Gender::class.java)
.withUnknownFallback(null)
.nullSafe()
)
.build()
I want to send data request via post to server I want to know How can I add data in array
data class City(
#SerializedName("cityId")
val cityId: Int?,
#SerializedName("detail")
val detail: List<String?>
)
Request
data class CityRequest(
#SerializedName("listCity")
val listCity: List<City?>
)
Response
data class CityResponse(
#SerializedName("code")
val code: String?,
#SerializedName("status")
val status: Boolean?,
#SerializedName("message")
val message: String?
)
API Server
#Headers("Content-Type: application/json")
#POST("city")
suspend fun sendCityContent(#Body listCity: CityRequest?):
Call<CityResponse?>
Connect Service
I don't know how I can add information to this section in question.
private suspend fun sendDataCity(city: List<city?>) {
val retrofit = clientCity
val sendDataToServer = retrofit?.create(CityService::class.java)
val call = sendDataToServer?.sendCityContent(CityRequest(city))
call?.enqueue(object : Callback<CityResponse?> {
override fun onResponse(
call: Call<CityResponse?>, response: Response<CityResponse?>) {
val getResponse = response.body()
Timber.tag("SALE_CITY: ").d("code: %s", getResponse?.code)
Timber.tag("SALE_CITY: ").d("status: %s", getResponse?.status)
Timber.tag("SALE_CITY: ").d("message: %s", getResponse?.message)
}
override fun onFailure(call: Call<CityResponse?>, t: Throwable?) {
t?.printStackTrace()
}
})
}
JSON Simple
{
"city": [
{
"cityId": 1,
"answer": [
"1"
]
},
{
"questionId": 2,
"answer": [
"2.1",
"2.2"
]
}
]}
What do I have to do next?
Can you have a sample add data in array for me?
Things I want
cityId = 1
detail = "1.1", "1.2"
cityId = 2
detail = "2.1", "2.2"
thank you
One issue i can see with your request is the key is different from what you are sending might be different check that. it should be city not listCity as given.
data class CityRequest(
#SerializedName("city")
val city: List<City?>
)
and your city class should have these keys answer which you have mentioned as details
data class City(
#SerializedName("cityId")
val cityId: Int?,
#SerializedName("answer")
val answer: List<String?>
)
I guess you are just sending with wrong keys that might be the reason the server is not accepting the request. make the above change it should work post if you get error.
This is what my json looks like
{
"sub": "9",
"auth_time": 1559381757,
"idp": "idsrv",
"role": [
"Employer",
"Employee",
"Student"
],
"iss": "",
"aud": "",
"exp": 1574933757,
"nbf": 1559381757
}
This is the object I want to convert this Json into.
data class Claims (
#SerializedName("nameid") val nameId: String,
#SerializedName("unique_id") val uniqueId: String,
#SerializedName("sub") val sub: String,
#SerializedName("unifiedNumber") val unifiedNumber: String,
#SerializedName("role") var roleList: List<Role>
)
I wrote a custom Deserializer (which works in Java) for the List type
class RoleDeserializer : JsonDeserializer<List<Role>> {
private var roleId = 0
#Throws(JsonParseException::class)
override fun deserialize(json: JsonElement, typeOfT: Type, context: JsonDeserializationContext): MutableList<Role> {
val resultList = ArrayList<Role>()
if (json.isJsonArray) {
for (e in json.asJsonArray) {
resultList.add(Role(id = roleId++, name = e.asString))
}
} else if (json.isJsonObject) {
resultList.add(Role(id = roleId++, name = json.asString))
} else if (json.isJsonPrimitive) {
if ((json as JsonPrimitive).isString)
resultList.add(Role(id = roleId++, name = json.getAsString()))
} else {
throw RuntimeException("Unexpected JSON type: " + json.javaClass)
}
return resultList
}
}
This is how I register my type adapter
val listType: Type = object : TypeToken<List<Role>>() {}.type
val gson = GsonBuilder().registerTypeAdapter(listType, RoleDeserializer()).create()
val claims = gson.fromJson(stringJson, Claims::class.java)
I still get a parse exception stating that
java.lang.IllegalStateException: Expected BEGIN_OBJECT but was STRING at line 1 column 161 path $.role[0]
and my RoleDeserializer is never called. Am I doing something wrong while registering the type adapter?
Try to replace
val listType: Type = object : TypeToken<List<Role>>() {}.type
with
val listType: Type = object : TypeToken<MutableList<Role>>() {}.type
The role is String array in JSON
Use this
#SerializedName("role") var roleList: List<String>
Instead of this
#SerializedName("role") var roleList: List<Role>
Try this
data class Claims (
#SerializedName("nameid") val nameId: String,
#SerializedName("unique_id") val uniqueId: String,
#SerializedName("sub") val sub: String,
#SerializedName("unifiedNumber") val unifiedNumber: String,
#SerializedName("role") var roleList: List<String>
)
I have one issue about code data class kotlin android.
How to implement server response? sometimes I get String value or sometime get Object class.
class CMSRespTemp {
data class CMSRespApi(
val status: Boolean = false,
val message: String = "",
val data: String as Data
)
data class Data(
val cms_id: String = "",
val cms_content: String = ""
)
}
When I implement only Data class it works, like this val data: Data or val data: String. But I need together Data and String with key only data.
Is it possible?
When having multiple type for same variable, we can use Any type which is equivalent to Object type in java. So solution is like below :
class CMSRespTemp {
data class CMSRespApi(
val status: Boolean = false,
val message: String = "",
var data: Any? = null // changed it to var from val, so that we can change it's type runtime if required
)
data class Data(
val cms_id: String = "",
val cms_content: String = ""
)
}
And when accessing that variable, one can simply cast like below :
val apiResponse : CMSRespApi //= some API response here from network call
when (apiResponse.data) {
is String -> {
// apiResponse.data will be smart-casted to String here
}
else -> {
val responseData = Gson().fromJson<CMSRespApi.Data>(
Gson().toJsonTree(apiResponse.data),
CMSRespApi.Data::class.java
)
}
}
After 12 Hrs spend and got the solution my self,
val getResultCon = getSerCont.result // response Any
val gson = Gson()
val jsonElement = gson.toJsonTree(getResultCon)
val resultData = gson.fromJson(jsonElement, SearchContactApi.Result::class.java)
Convert your data string to toJsonTree and fromJson with model class then got result.
I use Moshi for parse json from server. if server send null for item default value not set! but server not send that item default value set.
json:
{"percentChange": null,"change": "-2500.00","value": "130000","name": null}
data class:
#JsonClass(generateAdapter = true) data class Reference(val name:String? = "-",val value: Double,val change: Double,val percentChange: Double? = -10.0,)
but data for name and percentChange is null that should "-" for name and "-10.0" for percentChange. if server not send name and percentChange, default value work, but if send that null default value not work!
I use converter-moshi:2.4.0 and retrofit:2.4.0
This is working as intended because the null literal as a value for a key in JSON is semantically different than the absence of the key and value.
You can make a custom JsonAdapter for your use case.
#JsonClass(generateAdapter = true)
data class Reference(
#Name val name: String = "-",
val value: Double,
val change: Double,
val percentChange: Double? = -10.0
) {
#Retention(RUNTIME)
#JsonQualifier
annotation class Name
companion object {
#Name #FromJson fun fromJson(reader: JsonReader, delegate: JsonAdapter<String>): String {
if (reader.peek() == JsonReader.Token.NULL) {
reader.nextNull<Unit>()
return "-"
}
return delegate.fromJson(reader)!!
}
#ToJson fun toJson(#Name name: String): String {
return name
}
}
}
#Test fun reference() {
val moshi = Moshi.Builder()
.add(Reference)
.build()
val adapter = moshi.adapter(Reference::class.java)
val decoded = Reference("-", 130_000.toDouble(), (-2_500).toDouble(), null)
assertThat(adapter.fromJson(
"""{"percentChange": null,"change": "-2500.00","value": "130000"}"""))
.isEqualTo(decoded)
assertThat(adapter.fromJson(
"""{"percentChange": null,"change": "-2500.00","value": "130000","name": null}"""))
.isEqualTo(decoded)
}