Search code examples
jsonkotlinserializationktor

How to use @Serializer on deeper JSON datastructures


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
    }
}

Solution

  • 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,
    )