Hiya!
This is my current problem. I have a third party API which returns data for me. Now the API can return 2 completely different response bodies based on if request was successful or not. So my function has a union return type. In my example, lets call the return types A
and B
. In my case even though I have created classes A
and B
myself it does not make sense for them to know about how they are used. I want to avoid making A
or B
implement 10-20 different classes.
From what I see there are some possible solutions:
- Class composition with nullable types (doesn’t guarantee you don’t have all nulls so can lead to NPE)
- Generic class that stores generic object (doesn’t provide exhaustion when handling cases due to
Any
type)
- Sealed interfaces / classes
I went with the sealed interface approach and want to confirm that this is currently the best Kotlin JVM solution available in the absence of language supported union types.
Service.kt:
fun myApiQuery(): QueryResponse {
...
if (apiCallWasSuccessful) {
return QueryResponse.AWrapper(obj) // obj here is serialised success response of type `A` from API
}
else {
return QueryResponse.BWrapper(obj) // obj here is serialised error response of type `B` from API
}
}
QueryResponse.kt:
sealed interface QueryResponse {
data class AWrapper(
val response: A
) : QueryResponse
data class BWrapper(
val response: B,
) : QueryResponse
}
ConsumerUsageExample.kt:
fun main() {
val myApiQueryResponse = Service.myApiQuery()
check(myApiQueryResponse.response is AWrapper)
val a: A = myApiQueryResponse.response
// Continue using A as required
// Alternatively, could use same logic to use B
// or a when clause to handle multiple cases
}
If external API returns many variation of the request then I would advise to get
response in the form JsonElement like so… You can then check internal structure of JsonElement
and transform JsonElement into a apropriate class object…
In my case I joust use the raw JsonElement because why bother?
val res = client.get("https://something.com/data")
val body: JsonElement = res.body<JsonElement>()
and also to create helper functions on Json primitives to ease the development…
public fun JsonElement.obj(key: String): JsonObject = (this as JsonObject).get(key = key)!! as JsonObject
public fun JsonElement.array(key: String): JsonArray = (this as JsonObject).get(key = key)!! as JsonArray
public fun JsonElement.ele(key: String): JsonPrimitive = (this as JsonObject).get(key = key)!! as JsonPrimitive
body.obj(key = "forecast1h")
.array(key = "features").first()
.obj("properties")
.array("days").flatMap { day ->
day.array("timeline").map { time ->
WeatherInfo(
instant = Instant.parse(time.ele("valid").content),
temperature = time.ele("t").int,
clouds =
WeatherInfo.Clouds(
condition = WeatherInfo.Clouds.Condition.entries.first { it.value == time.ele("wwsyn_decodeText").content },
icon = time.ele("clouds_icon_wwsyn_icon").content,
description = time.ele("clouds_shortText").content,
height = time.ele("cloudBase_shortText").content,
),
wind =
WeatherInfo.Wind(
description = time.ele("ff_shortText").content,
direction = time.ele("dd_shortText").content,
speed = time.ele("ff_val").int,
),
humidity =
WeatherInfo.Humidity(
description = time.ele("rh_shortText").content,
percentage = time.ele("rh").int,
),
pressure =
WeatherInfo.Presure(
description = time.ele("pa_shortText").content,
value = time.ele("msl").int,
),
)
}
}
Yeah I think it’s the best option. Tbh, it sounds like you’re basically describing a Monad. I’ve actually come across the Arrow library before, which seems to do what you would want it to do: Either & Ior | Arrow