I'm using KTOR as a Http Client for my CMP app. I'm wondering if somehow I can omit wrapper class when fetching data from an API and directly load them into the DTO.
Below is my setup:
API response:
"data": {
"id": 11002,
"name": "name",
"category": "category"
}
Wrapper class:
@Serializable
data class SearchResponse(
val data: Dto
)
And finally, the DTO which has structure exactly the same as response:
@Serializable
data class Dto(
val id: String,
val name: String,
val category: String?
)
Whenever I make a call without the wrapper class, KTOR returns an error. When the wrapper class is included, everything works fine. Is there a way to omit the wrapper class and use the DTO directly?
Error:
io.ktor.serialization.JsonConvertException: Illegal input: Fields [id, name, category] are required for type with serial name 'com.example.dto', but they were missing at path: $
Your API call with the wrapper class works because the form of the response matches the standard JSON-serialized form of your SearchResponse
class.1
You can use the Dto
class directly if you write a KSerializer
for Dto
that will deserialize the same JSON to Dto
.
One technique for doing this is to use the surrogate serializer pattern, although it's a bit more complicated than usual because to do this we would like to use two serializers for Dto
: the user-defined one that Ktor will use, and the regular generated one for serialization with the surrogate object (now moved to be DtoDeserializer.SearchResponse
). To do this we use the new feature which keeps the generated serializer when a custom one is used on Dto
, and in turn this approach is made possible by the fact serializers for generic classes require the serializers for their generic parameters to be passed in at runtime (hence why the surrogate object is generic).
This code illustrates such a solution. Note this only works in Kotlin 2.0.20 or later and Kotlin JSON serialization 1.7.2 or later.2
/**
* We keep the generated serializer so that we can use it at the same time as the user-defined serializer
* [DtoDeserializer]. This requires Kotlin 2.0.20 or later and Kotlin JSON serialization library [1.7.2](https://github.com/Kotlin/kotlinx.serialization/releases/tag/v1.7.2)
* or later.
*/
@OptIn(ExperimentalSerializationApi::class)
@Serializable(DtoDeserializer::class)
@KeepGeneratedSerializer
data class Dto(
val id: String,
val name: String,
val category: String?
)
class DtoDeserializer : KSerializer<Dto> {
private val searchResponseSerializer = SearchResponse.serializer(Dto.generatedSerializer())
override val descriptor: SerialDescriptor
get() = searchResponseSerializer.descriptor
override fun deserialize(decoder: Decoder): Dto {
return searchResponseSerializer.deserialize(decoder).data
}
override fun serialize(encoder: Encoder, value: Dto) {
error("Not used as Dto is not serialized by app")
}
/**
* We make this class generic so that we can supply the serializer for [T] (namely the serializer for
* [Dto] at runtime
*/
@Serializable
private data class SearchResponse<T>(
val data: T
)
}
1This is actually only the case if either your API actually returns a String
for the id
property. I presume this is a mistake in your writing of the question.
2In earlier versions you would need another surrogate class which looked exactly like Dto
to generate the required serializer for the data
property of DtoDeserializer.SearchResponse
.