This project is in a very early stage, use it at your own risk!
The generator can be used in projects with following scala versions: 2.12, 2.13 and 3.x
Why creating another openapi-generator when there is an official one? While the mentioned generator is generally a great project and serves well for many people its scala part has a few flaws in my opinion. There is no proper encoding for discriminators, neither support for other json libraries. The genereted code doesn't feel like native. These, as well as the other things, could (and probably will at some point) be implemented, but the size of the project and underlying templating engine(mustache) don't make it easier. Last but not least it is currently impossible to generate openapi code into src-managed directory (OpenAPITools/openapi-generator#6685). I think that, by extracting and focusing only on a scala related part, it can be done better.
- generate idiomatic scala code
- support popular json libararies from scala ecosystem
- support only sttp but do it well
- proper integration with sbt and other build tools
- support discriminators
- support error encoding
- support open products
- comparison to similar projects
Given following yaml:
openapi: 3.0.3
info:
title: Entities
version: "1.0"
paths:
/:
get:
operationId: getRoot
responses:
"200":
description: ""
content:
application/json:
schema:
$ref: "#/components/schemas/Person"
components:
schemas:
Person:
required:
- name
- age
type: object
properties:
name:
type: string
age:
type: integer
minimum: 11
it will be turned into:
trait CirceCodecs extends SttpCirceApi {
implicit val personDecoder: Decoder[Person] =
Decoder.forProduct2("name", "age")(Person.apply)
implicit val personEncoder: Encoder[Person] =
Encoder.forProduct2("name", "age")(p => (p.name, p.age))
}
object CirceCodecs extends CirceCodecs
case class Person(name: String, age: Int)
class DefaultApi(baseUrl: String, circeCodecs: CirceCodecs = CirceCodecs) {
import circeCodecs._
def getRoot(): Request[Person, Any] = basicRequest
.get(uri"$baseUrl")
.response(
fromMetadata(
asJson[Person].getRight,
ConditionalResponseAs(
_.code == StatusCode.unsafeApply(200),
asJson[Person].getRight
)
)
)
}
Currently, there is only an integration for sbt, but I hope to add support for mill in the nearest future.
In order to use this project, follow the usual convention, first add it to project/plugins.sbt
:
addSbtPlugin("io.github.ghostbuster91.sttp-openapi" % "sbt-codegen-plugin" % "0.1.0")
next, enable it for the desired modules in build.sbt
:
import SttpOpenApiCodegenPlugin._
enablePlugins(SttpOpenApiCodegenPlugin)
Generator will walk through all files in input directory and generate for each one respective code into output directory. Package name based on directory structure will be preserved.
Code generation can be configured by one of the following options:
sttpOpenApiOutputPath
- Directory for sources generated by sttp-openapi generator (default: target/scala-2.12/src_managed/
)
sttpOpenApiInputPath
- Input resources for sttp-openapi generator (default: ./resources
)
sttpOpenApiJsonLibrary
- Json library for sttp-openapi generator to use (currently only Circe
)
sttpOpenApiHandleErrors
- If true the generator will include error information in types (default: true
)
In the openapi specification there is a notion of discriminators. These objects are used to distinguishing between polymorphic instances of some type based on a given value.
This project takes advantage of them and generates json configs accordingly.
components:
schemas:
Entity:
oneOf:
- $ref: "#/components/schemas/Person"
- $ref: "#/components/schemas/Organization"
discriminator:
propertyName: name
mapping:
john: "#/components/schemas/Person"
sml: "#/components/schemas/Organization"
Person:
required:
- name
- age
type: object
properties:
name:
type: string
age:
type: integer
Organization:
required:
- name
type: object
properties:
name:
type: string
sealed trait Entity { def name: String }
case class Organization(name: String) extends Entity()
case class Person(name: String, age: Int) extends Entity()
trait CirceCodecs extends SttpCirceApi {
// codecs for Person and Organization omitted for readability
implicit val entityDecoder: Decoder[Entity] = new Decoder[Entity]() {
override def apply(c: HCursor): Result[Entity] = c
.downField("name")
.as[String]
.flatMap({
case "john" => Decoder[Person].apply(c)
case "sml" => Decoder[Organization].apply(c)
case other =>
Left(DecodingFailure("Unexpected value for coproduct:" + other, Nil))
})
}
implicit val entityEncoder: Encoder[Entity] = new Encoder[Entity]() {
override def apply(entity: Entity): Json = entity match {
case person: Person => Encoder[Person].apply(person)
case organization: Organization =>
Encoder[Organization].apply(organization)
}
}
}
In openapi error responses can be represented equally easily as success ones. That is also the case for the sttp client. If you are not a fan of error handling, you can disable that feature in generator settings.
openapi: 3.0.2
info:
title: Entities
version: "1.0"
paths:
/person:
put:
summary: Update an existing person
description: Update an existing person by Id
operationId: updatePerson
responses:
"400":
content:
application/json:
schema:
$ref: "#/components/schemas/ErrorModel"
"401":
content:
application/json:
schema:
$ref: "#/components/schemas/ErrorModel2"
components:
schemas:
ErrorModel:
required:
- msg
type: object
properties:
msg:
type: string
ErrorModel2:
required:
- msg
type: object
properties:
msg:
type: string
sealed trait UpdatePersonGenericError
case class ErrorModel(msg: String) extends UpdatePersonGenericError()
case class ErrorModel2(msg: String) extends UpdatePersonGenericError()
class DefaultApi(baseUrl: String, circeCodecs: CirceCodecs = CirceCodecs) {
import circeCodecs._
def updatePerson(): Request[
Either[ResponseException[UpdatePersonGenericError, CirceError], Unit],
Any
] = basicRequest
.put(uri"$baseUrl/person")
.response(
fromMetadata(
asJsonEither[UpdatePersonGenericError, Unit],
ConditionalResponseAs(
_.code == StatusCode.unsafeApply(400),
asJsonEither[ErrorModel, Unit]
),
ConditionalResponseAs(
_.code == StatusCode.unsafeApply(401),
asJsonEither[ErrorModel2, Unit]
)
)
)
}
In openapi specifications data models can be extended by arbitrary properties if needed.
To do that one has to specify additionalProperties
on particular model. At the same time on the call site special codecs need to be provided to support such types.
Luckily, sttp-openapi-generator will handle that as well.
openapi: 3.0.3
info:
title: Entities
version: "1.0"
paths:
/:
get:
operationId: getRoot
responses:
"200":
description: ""
content:
application/json:
schema:
$ref: "#/components/schemas/Person"
components:
schemas:
Person:
required:
- name
- age
type: object
properties:
name:
type: string
age:
type: integer
additionalProperties: true
trait CirceCodecs extends SttpCirceApi {
implicit val personDecoder: Decoder[Person] = new Decoder[Person]() {
override def apply(c: HCursor): Result[Person] =
for {
name <- c.downField("name").as[String]
age <- c.downField("age").as[Int]
additionalProperties <- c.as[Map[String, Json]]
} yield Person(
name,
age,
additionalProperties.filterKeys(_ != "name").filterKeys(_ != "age")
)
}
implicit val personEncoder: Encoder[Person] = new Encoder[Person]() {
override def apply(person: Person): Json = Encoder
.forProduct2[Person, String, Int]("name", "age")(p => (p.name, p.age))
.apply(person)
.deepMerge(
Encoder[Map[String, Json]].apply(person._additionalProperties)
)
}
}
case class Person(
name: String,
age: Int,
_additionalProperties: Map[String, Json]
)
Apart from official openApi generator which was mentioned in the Why? section there are other similar projects.
-
Guardrail can generate both client and server code. When it comes to client code generation it is similar to that project, although it supports http4s and akka-http, while this project focuses solely on sttp.
Contributions are more than welcome. This is an early stage project which means that everything is subject to change.
See the list of issues and pick one! Or report your own.
If you are having doubts on the why or how something works, don't hesitate to ask a question.