numberfour / openapi-scala

An opinionated library and SBT plugin for generating Scala code from OpenAPI 3.


OpenAPI Scala

This project contains an opinionated library and SBT Plugin for Scala code generation from OpenAPI 3.0 compliant YAML.

This will generate Components (i.e., schemas or types), and Paths (i.e., route definitions) for REST APIs.

We do not support all OpenAPI 3.0 features. For more details on what is supported, look at the Limitations section.

OpenAPI Scala

SBT sub-project openapi-scala contains the main logic and library for loading, representing, and translating YAML to Scala code. These contain components that are translated into Scala's case classes and Routes, which are explained in the next section.


We support different types of Route generation, depending on the backend you need it for.

  • The generic Routes are each translated to a Scala trait declaring interfaces for that particular HTTP route.
  • The http4s Routes translate into two files, one file with a trait for the implementation you'll need to provide and one file with an object apply function of which accepts mentioned implementation trait.

The names of the functions and values for the routes is generated at run-time. However, if an operationId field is defined (as specified in OAS 3.0) we will use those to generate user-friendly function names.


Components in OpenAPI are the types that can be referred to as inputs/outputs for routes. A common use of components is to define product types. We take these components and translate them to Scala case classes. Consider the following example:

            description: My test description
                    type: string
                    type: number

The code above will be translated to following Scala code:

 * My test description
final case class Person(name: String, weight: Double)

object Person {
    implicit val customDecoders = deriveDecoder[Person](renaming.snakeCase, None)
    implicit val customEncoders = deriveEncoder[Person](renaming.snakeCase, true, None)



SBT sub-project sbt-openapi contains an SBT plugin that allows the use of openapi-scala library to a given YAML file that will be loaded and used to generate managed Scala sources.


A release log of the plugin and the library can be found here.

To use the plugin, you first need to make it available in your SBT.

// project/plugins.sbt
addSbtPlugin("com.enfore" % "sbt-openapi" % "<openapi-scala-version>")

You will also need to make the library available to be able to use the generated code.

libraryDependencies += "com.enfore" %% "openapi-lib" % "<openapi-scala-version>"

Additionally, you will need to satisfy these library dependencies for openapi-lib. The provided versions are tested and made sure to work. You should be able to use any version compatible with your project though.

"com.beachape" %% "enumeratum"       % "1.5.13",
"com.beachape" %% "enumeratum-circe" % "1.5.20",
"io.circe"     %% "circe-derivation" % "0.11.0-M1",
"com.chuusai"  %% "shapeless"        % "2.3.3"

For http4s Routes you will also need:

"org.http4s" %% s"http4s-dsl" % "0.20.0-M7",
"org.http4s" %% s"http4s-circe" % "0.20.0-M7"

Once the plugin is available in your project you can enable it on a given an SBT sub-project and use the setting openAPIOutputPackage to specify the package name for your components.

// build.sbt
lazy val root = (project in ("."))
        openAPIOutputPackage := "com.enfore.model",
        libraryDependencies += "com.enfore" %% "openapi-lib" % "<openapi-scala-version>"

SBT Settings

Following are the settings available for your SBT project.

openAPISource: Source directory for OpenAPI. Defaults to src/main/openapi of the project.

openAPIOutput: Output directory for the OpenAPI. Defaults to managed sources — openapi.

openAPIOutputPacakge: Name of the package to be used for OpenAPI components.

routeImplementations: A List of com.enfore.apis.generator.RouteImplementation, which controls which kind of routes should be generated. Find out more about routes


OAS 3.0 allows one to set constraints on the primitive input types. We use the Refined library for Scala to reproduce these in the generated code. This means that you do not need to manually setup validations for any refinements and constraints in the inputs for your API. This, however, also means that you will have to return values with refinements if there are any in the return types.

In the example code below, we also generate a constructor for a value over which refinements exist. These refinements exist inside an object called RefinementConstructors that is further nested in the companion object for the generated case class. The names of the constructors will be the same as the values for which the refinements exist.

import shapeless._

final case class MyTestOutput(name: String Refined AllOf[MinSize[W.`1`.T] :: MaxSize[W.`256`.T] :: HNil])

object MyTestOutput {
    object RefinementConstructors {
        val name = String Refined AllOf[MinSize[W.`1`.T] :: MaxSize[W.`256`.T] :: HNil]


Read Only Properties

OpenAPI 3.0 supports marking properties in objects as read-only. This checks that the property is available if the type is used as the output of a route, but not for inputs.

We model this in Scala by generating two types in case a field is marked read-only. The generator will then automatically use the right type depending on weather the object is used as input or output.

    type: object
            type: string
            readOnly: true
            type: string

This will generate two Scala classes that are as follows:

final case class MyObjectRequest(body: String)

final case class MyObject(id: String, body: String)

Unions & Enums

OpenAPI 3.0 uses oneOf types to represent unions or enumerations. In Scala, there is no way to have proper Algebraic Data Types (ADTs). While products can be represented using case classes, the only way to represent unions is to create classes that extend a given trait.

This, however, makes defining arbitrary unions very difficult. Therefore, we use Shapeless' discriminated unions to represent anyOf types in Scala.

final case class MyUnionWrapper(value: Union)

object MyUnionWrapper {
    type Union = Item1 :+: Item2 :+: CNil

    object jsonConversions extends Poly1 {
        implicit def caseItem1 = at[SingleTargetConversion](_.asJson.deepMerge(Json.obj("@type" -> Json.fromString("Item1"))))
        implicit def caseItem2 = at[MultiTargetConversion](_.asJson.deepMerge(Json.obj("@type" -> Json.fromString("Item2"))))


    implicit val customerEncoders = new Encoder[MyUnionWrapper] {
      def apply(a: Conversion): Json = {
        (a.value map jsonConversions).unify

  implicit val customDecoder: Decoder[MyUnionWrapper] = new Decoder[Conversion] {
    def apply(c: HCursor): Decoder.Result[MyUnionWrapper] = {
      val output = c.downField("@type").as[String] match {
        case Right("Item1") =>[Item1].map(Coproduct[Union](_))
        case Right("Item2") =>[Item2].map(Coproduct[Union](_))
        case _ => Left(DecodingFailure.apply("Type information not available", List(CursorOp.DownField("@type"))))


Using these arbitrary unions is easier than it first appears. To create a value of type MyUnionWrapper, which is defined using oneOf in OpenAPI, we can use the following code:

import shapeless._
val item1Union = MyUnionWrapper(Coproduct[MyUnionWrapper.Union](item1))
val item2Union = MyUnionWrapper(Coproduct[MyUnionWrapper.Union](item2))

Handling the values of that are already wrapped and mapping them to something is slightly more work. We use the Poly1 from Shapeless to create functions that can map over a co-product.

object convertToT extends Poly1 {
    implicit val oneToT = at[Item1](x => oneToT(x))
    implicit val twoToT = at[Item2](x => twoToT(x))

val convertedItem: T = item1Union.fold(convertToT)

Our convertToT function converts every possible value to a type T and then we unify them. Since we know that our co-product will contain exactly one value of the possible given types, we can be certain that we will end up with at least one value of type T.


This generator is highly opinionated and skips over certain features defined OpenAPI 3.0 specification for the sake of API consistency and compatibility with modern typed languages.


  • We do not support any anonymous types for representing HTTP requests and responses.
  • We do not support nested type definitions. Therefore, any objects that are used must be defined in the components and must not contain any other nested objects. Nested arrays, however, are supported.
  • We do not support more than one type for Content-Type header for incoming requests. Multiple types corresponding to their encodings are, however, available with an Accept-Encoding header on a request.
  • We do not support anyOf types from OpenAPI 3.0. Suggestions as to how the behaviour for the same should be represented in generated Scala code are welcome.
  • We do not support inlined schema definition for input and output object types in route definitions in OpenAPI input files. Therefore, these must be defined as components and referred.
  • We do not support type aliasing or rich typing. For instance, if you have a primitive type with a particular set of refinements, you would have to point out those refinements everywhere with those refinements instead of referring them from a definition.