Skip to content

[kotlin-spring] Add type-safe response handling with sealed interfaces for multiple status codes #23004

@louislepper

Description

@louislepper

The Problem:

Currently, when a Kotlin Spring endpoint returns different response types for different status codes, there's no type-safe way to represent this in the generated interface. Controllers must use unsafe casts or return a generic supertype, losing compile-time type safety.

Current Behavior:

Given an OpenAPI spec with multiple response types:


paths:
  /users:
    post:
      operationId: createUser
      responses:
        '200':
          description: User created successfully
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/User'
        '409':
          description: Conflict - User already exists
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ConflictResponse'
        '400':
          description: Bad request
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponse'

The generated interface returns only the 200 response type:


interface DefaultApi {
    fun createUser(@Valid @RequestBody request: CreateUserRequest): ResponseEntity<User>
}

This forces implementations to use unsafe casts when returning non 2xx responses in controllers.

A Possible Solution:

Kotlin provides sealed interfaces, which feel like a good solution to this:

// Generated sealed interface
sealed interface CreateUserResponse

// Generated models implement the sealed interface
data class User(...) : CreateUserResponse
data class ConflictResponse(...) : CreateUserResponse
data class ErrorResponse(...) : CreateUserResponse

// Generated API uses sealed interface
interface DefaultApi {
    fun createUser(@Valid @RequestBody request: CreateUserRequest): ResponseEntity<CreateUserResponse>
}

This way our controllers can return any response code while maintaining compile time safety

@RestController
class UserController : DefaultApi {
    override fun createUser(request: CreateUserRequest): ResponseEntity<CreateUserResponse> {
        if (userExists(request.email)) {
            val response: CreateUserResponse = ConflictResponse(
                reason = ConflictReason.EMAIL_CONFLICT,
                message = "User already exists"
            )
            return ResponseEntity.status(409).body(response)
        }

        if (invalid(request)) {
            val response: CreateUserResponse = ErrorResponse(
                code = "INVALID_INPUT",
                message = "Invalid request"
            )
            return ResponseEntity.status(400).body(response)
        }

        val response: CreateUserResponse = User(...)
        return ResponseEntity.ok(response)
    }
}

Metadata

Metadata

Assignees

No one assigned

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions