Skip to main content

Defining response models

The response type must be annotated with @Serializable.

Basic response model example

@Serializable
data class UsersResponse(
val users: List<UserResponse>,
val total: Int,
)

@Serializable
data class UserResponse(
val id: Int,
val email: Email,
val role: Role,
val isActive: Boolean,
val createdAt: String,
val updatedAt: String,
)

Return types

Typed responses

Specify the response type using @Returns:

@GET("/users/{id}")
@Returns(User::class)
@Serializable
data class GetUserRequest(@Path @Transient val id: Int = 1)

The response type must be @Serializable:

@Serializable
data class User(
val id: Int,
val name: String,
val email: String
)

Generated method returns ApiResponse<User>:

suspend fun getUser(id: Int): ApiResponse<User>

Empty responses

For endpoints that return no body (e.g., DELETE):

@DELETE("/users/{id}")
@Returns(Unit::class)
@Serializable
data class DeleteUserRequest(@Path @Transient val id: Int = 1)

Generated method returns EmptyResponse (typealias for ApiResponse<Unit>):

suspend fun deleteUser(id: Int): EmptyResponse

ApiResponse class

All generated methods return ApiResponse<T> which provides:

class ApiResponse<T>(
val status: HttpStatusCode, // HTTP status code
val headers: Headers, // Response headers
val contentType: ContentType?, // Content type
val body: T // Deserialized body
) {
val isSuccess: Boolean // true if 2xx
val isClientError: Boolean // true if 4xx
val isServerError: Boolean // true if 5xx

fun header(name: String): String? // Get specific header
fun requireSuccess(): ApiResponse<T> // Throws if not 2xx
}

Error type handling

For APIs that return structured error responses, you can specify an optional error type in the @Returns annotation:

@Serializable
data class ErrorResponse(
val code: String,
val message: String,
val details: Map<String, String>? = null
)

@POST("/auth/login")
@Returns(success = LoginResponse::class, error = ErrorResponse::class)
@Serializable
data class LoginRequest(
var email: String? = null,
var password: String? = null
)

When an error type is specified, the processor generates a sealed result type:

public sealed interface LoginResult {
data class Success(val body: LoginResponse, val response: HttpResponse) : LoginResult
data class Error(val body: ErrorResponse, val response: HttpResponse) : LoginResult
}

The generated method returns this sealed type:

suspend fun login(block: LoginRequest.() -> Unit): LoginResult

Usage

Normal usage:

// Testing success case - clean and focused
val result = client.login {
email = "user@example.com"
password = "secret"
}.requireSuccess()

println("Token: ${result.body.token}")
// Access raw HTTP response if needed: result.response

// Testing error case - equally clean
val errorResult = client.login {
email = "invalid@example.com"
password = "wrong"
}.requireError()

println("Error ${errorResult.body.code}: ${errorResult.body.message}")
// Access raw HTTP response: errorResult.response

For cases where you need to handle both outcomes:

when (val result = client.login { email = "user@example.com"; password = "secret" }) {
is LoginResult.Success -> println("Token: ${result.body.token}")
is LoginResult.Error -> println("Error ${result.body.code}: ${result.body.message}")
}

Requirements:

  • The error type must be annotated with @Serializable
  • If the error response cannot be deserialized (unexpected format), an IllegalStateException is thrown