Package-level declarations

Entry pointRole
problemResponsesOn a Route: attach a catalog's universal responses to that route and everything below it. One call inside routing { } covers the whole application.
problemsFromInside responses { }: document the half of a ProblemDetailsCatalog that holds for every operation — the catch-all and its registered status codes.
problemResponseInside responses { }: document one problem type at its own status, or a bare status code.
problemDefaultInside responses { }: document the catch-all default response as a problem document.
ProblemSchemasThe RFC 9457 JSON Schemas — problem and the errors[]-carrying problemWithErrors. Hand-written, because the serializer's descriptor describes neither optionality nor extension members.

Published for jvm, js, wasmJs, linuxX64, linuxArm64, mingwX64, macosArm64, iosArm64 and iosSimulatorArm64. Depend on the plain coordinates from commonMain and Gradle picks the variant.

Usage

val catalog = problemCatalog { standardStatusCodes() }

install(StatusPages) { problemDetails(catalog) }

routing {
// Every endpoint below now documents the catch-all and the catalog's status codes.
problemResponses(catalog)

get("/orders/{id}") { call.respondText("an order") }
.describe { responses { problemResponse(OutOfCredit) } }
}

problemResponses wraps Ktor's experimental route-description API. Opt in at the calling declaration or file with @OptIn(ExperimentalProblemDetailsOpenApi::class). The lower-level schema and response builders do not require this opt-in.

Ktor folds route metadata from the routing root downwards, so the one call at the root reaches every endpoint, and a describe on a leaf merges with it rather than replacing it. Call problemResponses on a route("/api") { } instead when only a subtree should carry them.

The schema lands in components/schemas/ProblemDetails and every response references it — Ktor hoists any titled schema on its own, so no component registration is needed here.

What a catalog can and cannot tell you

A ProblemDetailsCatalog is installed once for the whole application and records no route. Two halves of it therefore document very differently.

The catch-all and every status registered through forStatusCode/standardStatusCodes() hold wherever the catalog is installed — any route can 404, any unhandled failure answers with a problem document. problemsFrom attaches exactly those.

A type declared with map<InsufficientFunds>(OutOfCredit) does not. Attaching it everywhere would document GET /health as returning 403 and make a client generator emit handling for it. Name such a type where it can actually occur, with problemResponse.

Two problem types on one status

OpenAPI allows a single response object per status code, so declaring two types that share one — a 403 that is either "out of credit" or "account locked" — cannot produce two entries. The two calls collapse into one response whose description names both, carrying one example per type keyed by its type URI. Nothing is silently dropped, and a client generator still sees a single 403 shape, which is what the wire actually offers.

Why the schemas are not inferred

jsonSchema<Problem>() would read ProblemSerializer's descriptor, which declares type non-optional and has no way to describe extension members at all — they are decided at runtime. The result would require a member RFC 9457 §3.1 makes optional and omit the sibling extensions of §3.2 entirely.

Each schema carries a non-null title, and that is load-bearing: Ktor hoists a titled schema into components/schemas and rewrites every use to a $ref. Clearing the title inlines the whole schema at every response instead.

Types

Link copied to clipboard
@RequiresOptIn(level = RequiresOptIn.Level.ERROR, message = "This API wraps Ktor's experimental route-description API and may change with it.")
annotation class ExperimentalProblemDetailsOpenApi

Marks API that depends on Ktor's experimental route-description API.

Link copied to clipboard

The JSON Schemas this module writes into an OpenAPI document.

Functions

Link copied to clipboard
fun Responses.Builder.problemDefault(description: String? = null, schema: JsonSchema = ProblemSchemas.problem, configure: Response.Builder.() -> Unit = {})

Documents the catch-all default response as a problem document.

Link copied to clipboard
fun Responses.Builder.problemResponse(type: ProblemType, description: String? = null, schema: JsonSchema = ProblemSchemas.problem, configure: Response.Builder.() -> Unit = {})

Documents type as a possible outcome of this operation.

fun Responses.Builder.problemResponse(status: HttpStatusCode, description: String? = null, schema: JsonSchema = ProblemSchemas.problem, configure: Response.Builder.() -> Unit = {})

Documents status as producing a problem document, without naming a problem type.

Link copied to clipboard

Documents catalog's universally-applicable responses on this route and everything below it.

Link copied to clipboard

Documents the part of catalog that is true of every operation.