Is a sealed interface the best way to handle union types

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