dephinera
11/14/2023, 3:33 PM{
"error": {
"code": "ERROR_CODE",
"message": "Error message"
},
"errorDetails": {
"missingDetails": [
"ADDRESS",
"NAME"
]
}
}
or like this
{
"error": {
"code": "ERROR_CODE",
"message": "Error message",
"invalidAttributes": {
"body": {
"missingDetails": [
"ADDRESS",
"NAME"
]
}
}
}
}
Depending on the “code” I want to deserialize to one of two subclasses of a sealed interface
sealed interface ApiError {
data class Common(...) : ApiError
data class Specific(...) : ApiError)
}
I started implementing a JsonTransformingSerializer
to normalize the json so I can handle both response formats. The custom JsonTransformingSerializer
is for ApiError
- ApiErrorSerializer : JsonTransformingSerializer<ApiError>(ApiError.serializer())
.
Now I have the following issues:
1. I can deserialize only by passing the serializer to the json - aka like this json.decodeFromString(ApiErrorSerializer, jsonString)
2. Annotating ApiError with @Serializable(with = ApiErrorSerializer::class)
doesn’t work, as ApiErrorSerializer
relies on the generated serializer. There’s no way set ApiErrorSerializer
to be used globally
3. Things work if the json is wrapped in another property and therefore if my model is wrapped as well. Something like this:
a. @Serializable data class Result(@Serializable(ApiErrorSerializer::class) val result: ApiError)
I guess the key part here is that I need to operate on the whole object, so in the serializer I have access to both "error"
and "errorDetails" (if present)
and I want to unwrap the object so neither ApiError.Common
nor ApiError.Specific
have an error
property but rather its inner properties.
What’s the best way to achieve this? I don’t have control over the API unfortunately. Is there a way to achieve this by both declaring the serializer globally and without wrapping the whole thing? Is there a way I can both declare a custom serializer for ApiError
and delegate to it’s default one?chr
11/14/2023, 7:58 PM@JvmInline
@Serializable
value class ApiErrorResponse(
@Serializable(with = ApiErrorSerializer::class)
val apiError: ApiError
)
val apiError = json.decodeFromString<ApiErrorResponse>(jsonString).apiError
dephinera
11/14/2023, 8:02 PMchr
11/14/2023, 8:02 PMdephinera
11/14/2023, 8:08 PMchr
11/14/2023, 8:10 PMtypealias ApiErrorResponse = @Serializable(with = ApiErrorSerializer::class) ApiError
Then you wouldn’t need a wrapper at all, I think, as long as you declare the type as `ApiErrorResponse`:
val apiError = json.decodeFromString<ApiErrorResponse>(jsonString)
dephinera
11/14/2023, 8:14 PM@Serializable(with = ApiErrorSerializer::class)
sealed interface ApiError
which doesn’t work as ApiErrorSerializer delegates to ApiError.serializer()
json.decodeFromString
and it can operate at the higher level of the json, but the same deserializer can’t be set as default one for the type, without overriding the generated serializer. Perhaps it’s the explicitness that was desired but still…Joshua Hansen
11/14/2023, 10:41 PMJsonTransformingSerializer
still relies on the plugin-generated serializer to operate. If you don't mark the class simply with @Serializable
then there won't be a plugin-generated serializer to delegate to. blob shrug
The value class option is probably the best one if you don't want to pass the serializer manually every time.
That, or just write a wrapper function which does it.
companion object {
val JSON = Json { /* config */ }
fun encodeMyClassToString(toEncode: MyClass): String {
return JSON.encodeToString(MySerializer, toEncode)
}
}
You could extend it further if you have a bunch of classes and create a map of class to serializer which the function uses or something like that. But that's quite a bit of noise just to not pass in a serializer each time. And wrapping APIs around APIs is probably not a good idea for most cases since it obfuscates your logic/intentions with your code.dephinera
11/15/2023, 8:13 AMalways pass the serializer
approach is not viable because I can’t pass it when using the ktor json content negotiation plugin, as the call to encode/decode happens internally. I guess that leaves the value class as the only optionJsonContentPolymorphicSerializer
on the sealed interface I can scan the incoming JsonElement
and manually call the default serializer for the according sealed interface child. That however doesn’t allow me to modify the json element itself, so I can pass the normalized tree to the delegate serializers. I ended up using a KSerializer<ApiCallError<T>>
that receives a Decoder
which I then cast to JsonDecoder
. I then decode the JsonElement
from it, create a new Json object with the desired structure and use the decoder.json
instance to decode from the transformed json. This way I don’t need a wrapper and it’s not a problem to override the default serializer of the sealed interface. I just delegate to the ones of the sealed interface children