Typesafe contracts (OpenAPI3)

Installation (Gradle)

dependencies {
    implementation(platform("org.http4k:http4k-bom:5.14.4.0"))
    implementation("org.http4k:http4k-contract")
    implementation("org.http4k:http4k-format-<insert json lib>")
}

About

The http4k-contract module adds a much more sophisticated routing mechanism to that available in http4k-core. It adds the facility to declare server-side Routes in a completely typesafe way, leveraging the Lens functionality from the core. These Routes are combined into Contracts, which have the following features:

  • Auto-validating - the Route contract is automatically validated on each call for required-fields and type conversions, removing the requirement for any validation code to be written by the API user. Invalid calls result in a HTTP 400 (BAD_REQUEST) response.
  • Self-describing: - a generated endpoint is provided which describes all of the Routes in that module. Implementations include OpenApi v2 & v3 documentation, including generation of JSON schema. These documents can then be used to generate HTTP client and server code in various languages using the OpenAPI generator. models for messages.
  • Security: to secure the Routes against unauthorised access. Current implementations include ApiKey, BasicAuth, BearerAuth, OpenIdConnect and OAuth.
  • Callbacks and Webhooks can be declared, which give the same level of documentation and model generation

Code

package guide.reference.contracts

// for this example we're using Jackson - note that the auto method imported is an extension
// function that is defined on the Jackson instance

import org.http4k.contract.ContractRoute
import org.http4k.contract.bind
import org.http4k.contract.bindCallback
import org.http4k.contract.contract
import org.http4k.contract.div
import org.http4k.contract.meta
import org.http4k.contract.openapi.ApiInfo
import org.http4k.contract.openapi.v3.OpenApi3
import org.http4k.contract.security.ApiKeySecurity
import org.http4k.core.Body
import org.http4k.core.ContentType.Companion.TEXT_PLAIN
import org.http4k.core.HttpHandler
import org.http4k.core.Method.GET
import org.http4k.core.Method.POST
import org.http4k.core.Request
import org.http4k.core.Response
import org.http4k.core.Status.Companion.OK
import org.http4k.core.Uri
import org.http4k.core.with
import org.http4k.format.Jackson
import org.http4k.format.Jackson.auto
import org.http4k.lens.Path
import org.http4k.lens.Query
import org.http4k.lens.int
import org.http4k.lens.string
import org.http4k.routing.routes

// this route has a dynamic path segment
fun greetRoute(): ContractRoute {

    // these lenses define the dynamic parts of the request that will be used in processing
    val ageQuery = Query.int().required("age")
    val stringBody = Body.string(TEXT_PLAIN).toLens()

    // this specifies the route contract, with the desired contract of path, headers, queries and body parameters.
    val spec = "/greet" / Path.of("name") meta {
        summary = "tells the user hello!"
        queries += ageQuery
        receiving(stringBody)
    } bindContract GET

    // the this function will dynamically supply a new HttpHandler for each call. The number of parameters
    // matches the number of dynamic sections in the path (1)
    fun greet(nameFromPath: String): HttpHandler = { request: Request ->
        val age = ageQuery(request)
        val sentMessage = stringBody(request)

        Response(OK).with(stringBody of "hello $nameFromPath you are $age. You sent $sentMessage")
    }

    return spec to ::greet
}

data class NameAndMessage(val name: String, val message: String)

// this route uses auto-marshalling to convert the JSON body directly to/from a data class instance
fun echoRoute(): ContractRoute {

    // the body lens here is imported as an extension function from the Jackson instance
    val body = Body.auto<NameAndMessage>().toLens()

    // this specifies the route contract, including examples of the input and output body objects - they will
    // get exploded into JSON schema in the OpenAPI docs
    val spec = "/echo" meta {
        summary = "echoes the name and message sent to it"
        receiving(body to NameAndMessage("jim", "hello!"))
        returning(OK, body to NameAndMessage("jim", "hello!"))
    } bindContract POST

    // note that because we don't have any dynamic parameters, we can use a HttpHandler instance instead of a function
    val echo: HttpHandler = { request: Request ->
        val received: NameAndMessage = body(request)
        Response(OK).with(body of received)
    }

    return spec to echo
}

// this route has a callback registered, so can be used when processes have asynchronous updates
// they will be POSTed back to callbackUrl received in the request
fun routeWithCallback(): ContractRoute {

    val body = Body.auto<StartProcess>().toLens()

    val spec = "/callback" meta {
        summary = "kick off a process with an async callback"

        // register the callback for later updates. The syntax of the callback URL comes
        // from the OpenApi spec
        callback("update") {
            """{${"$"}request.body#/callbackUrl}""" meta {
                receiving(
                    body to StartProcess(Uri.of("http://caller"))
                )
            } bindCallback POST
        }
    } bindContract POST

    val echo: HttpHandler = { request: Request ->
        println(body(request))
        Response(OK)
    }

    return spec to echo
}

data class StartProcess(val callbackUrl: Uri)

// use another Lens to set up the API-key - the answer is 42!
val mySecurity = ApiKeySecurity(Query.int().required("reference/api"), { it == 42 })

// Combine the Routes into a contract and bind to a context, defining a renderer (in this example
// OpenApi/Swagger) and a security model (in this case an API-Key):
val contract = contract {
    renderer = OpenApi3(ApiInfo("My great API", "v1.0"), Jackson)
    descriptionPath = "/openapi.json"
    security = mySecurity
    routes += greetRoute()
    routes += echoRoute()
    routes += routeWithCallback()
}

val handler: HttpHandler = routes("/reference/api/v1" bind contract)

// by default, the OpenAPI docs live at the root of the contract context, but we can override it..
fun main() {
    println(handler(Request(GET, "/reference/api/v1/openapi.json")))

    println(
        handler(
            Request(POST, "/reference/api/v1/echo")
                .query("reference/api", "42")
                .body("""{"name":"Bob","message":"Hello"}""")
        )
    )
}

When launched, OpenApi format documentation (including JSON schema models) can be found at the route of the module.

For a more extended example, see the following example apps:

Naming of JSON Schema models

There are currently 2 options for JSON schema generation.

  1. OpenApi v2 & v3: The standard mechanism can be used with any of the supported http4k JSON modules. It generates anonymous JSON schema definition names that are then listed in the schema section of the OpenApi docs.
    OpenApi3(ApiInfo("title", "1.2", "module description"), Argo)

... generates definitions like the following in the schema definitions:

{
  "components": {
    "schemas": {
      "object1283926341": {
        "type": "object",
        "properties": {
          "aString": {
            "type": "string"
          }
        }
      }
    }
  }
}
  1. OpenApi v3 only: By including a supported Auto-JSON marshalling module on the classpath (currently only http4k-format-jackson), the names of the definitions are generated based on the Kotlin class instances provided to the Contract Route DSL. Note that an overloaded OpenApi function automatically provides the default Jackson instance, so we can remove it from the renderer creation:
    OpenApi3(ApiInfo("title", "1.2", "module description"), Jackson)

... generates definitions like the following in the schema definitions:

{
   "components":{
      "schemas":{
          "ArbObject": {
            "properties": {
              "uri": {
                "example": "http://foowang",
                "type": "string"
              }
            },
            "example": {
              "uri": "http://foowang"
            },
            "type": "object",
            "required": [
              "uri"
            ]
          }
      }
   }
}

Receiving Binary content with http4k Contracts (application/octet-stream or multipart etc)

With binary attachments, you need to turn ensure that the pre-flight validation does not eat the stream. This is possible by instructing http4k to ignore the incoming body for validation purposes:

routes += "/api/document-upload" meta {
    preFlightExtraction = PreFlightExtraction.IgnoreBody
} bindContract POST to { req -> Response(OK) }