Reputation: 857
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
}
}
Upvotes: 1
Views: 199
Reputation: 723
In case anyone else has the same or similar question, KotlinX serialization does support extracting fields as originally asked using a custom serializer. https://github.com/Kotlin/kotlinx.serialization/blob/master/docs/serializers.md#custom-serializers
For example, with the JSON given in the original question, you can use a custom serializer for List like this:
@Serializable
data class SitesSurrogate(
val sites: Sites?,
)
@Serializable
data class Sites(
val count: Int?,
val site: List<Site>?,
)
@Serializable
data class Site(
val id: Int?,
val name: String?,
val accountId: Int?,
)
object SiteSerializer : KSerializer<List<Site>?> {
override val descriptor: SerialDescriptor = SitesSurrogate.serializer().descriptor
@OptIn(ExperimentalSerializationApi::class)
override fun deserialize(decoder: Decoder): List<Site>? {
return decoder.decodeNullableSerializableValue(SitesSurrogate.serializer())?.sites?.site
}
override fun serialize(encoder: Encoder, value: List<Site>?) {
encoder.encodeSerializableValue(SitesSurrogate.serializer(), SitesSurrogate(Sites(value?.size, value)))
}
}
And then deserialize the json like this:
val sites: List<Site>? = Json.decodeFromString(SiteSerializer, json)
This is perhaps not the best use case for this feature, but works great if you need to extract embedded fields to a more flat format. For example, for the following JSON:
{
"id": "12345",
"chatId": "67890",
"createdDateTime": "2018-01-22T15:54:12.532Z"
"from": {
"user": {
"displayName": "Bill S. Preston, Esq."
}
}
}
You can extract displayName
out to be just the value of the from
field with Json.decodeFromString<ImportedMessage>(json)
like this:
@Serializable
data class ImportedMessage(
val id: String?,
val chatId: String?,
val createdDateTime: String?,
@Serializable(with = FromSerializer::class) val from: String?,
)
@Serializable
data class FromSurrogate(
val user: User?,
)
@Serializable
data class User(
val displayName: String?,
)
object FromSerializer : KSerializer<String?> {
override val descriptor: SerialDescriptor = FromSurrogate.serializer().descriptor
@OptIn(ExperimentalSerializationApi::class)
override fun deserialize(decoder: Decoder): String? {
return decoder.decodeNullableSerializableValue(FromSurrogate.serializer())?.user?.displayName
}
override fun serialize(encoder: Encoder, value: String?) {
encoder.encodeSerializableValue(FromSurrogate.serializer(), FromSurrogate(User(value)))
}
}
Upvotes: 0
Reputation: 5474
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,
)
Upvotes: 1
Reputation: 11
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
)
}}
Upvotes: 0