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
IllegalStateExceptionis thrown