An enhancement to springdoc-openapi that adds better support for Scala.
Scala isn't well-supported in springdoc-openapi by default, for example:
case classparameters are not recognized by default, one has to add something like@BeanPropertyto each- even with
@BeanProperty, most parameters with generic type (likeOption) don't work correctly - even with
@BeanProperty, all parameters are marked as not required in generated OpenAPI docs - Spring endpoints returning
Unitare not "No Content" but instead show that the endpoint returnsBoxedUnit
One option to overcome these limitations is to annotate the model with annotations provided by springdoc-openapi.
But even with them, @BeanPropery or equivalent must be added to each case class parameter.
This library aims to avoid pollution of the model by custom annotations and dependency on Spring related libraries.
- all parameters of a
case classare automatically recognized without any custom annotations - all parameters of a
case classthat have a type different fromOptionare marked as required - Spring endpoints returning Unit are "No Content"
- support for basic Scala collections (
Map,Seq,Set,Array) as types ofcase classparameters - only top-level case classes need to be registered, child case classes are then recursively registered
- support for Scala
Enumerationwhere simpleValueconstructor is used (withoutname) - support for sum ADTs (
sealed traitandsealed abstract class) with optional discriminator
springdoc-openapi-scala supports two major versions of springdoc-openapi: 1.x and 2.x.
The library has springdoc-openapi as a provided dependency, thus users of the library have to include that dependency in their projects:
- for springdoc-openapi 1.x versions
1.6.7up to1.7.0(including) of"org.springdoc" % "springdoc-openapi-webmvc-core"are supported - for springdoc-openapi 2.x versions
2.0.0onwards of"org.springdoc" % "springdoc-openapi-starter-webmvc-api"are supported
If you want to use the library with springdoc-openapi 1.x, add:
libraryDependencies ++= Seq("za.co.absa" %% "springdoc-openapi-scala-1" % VERSION)if with springdoc-openapi 2.x, add:
libraryDependencies ++= Seq("za.co.absa" %% "springdoc-openapi-scala-2" % VERSION)If you want to use the library with springdoc-openapi 1.x, add:
<dependency>
<groupId>za.co.absa</groupId>
<artifactId>springdoc-openapi-scala-1_${scala_binary_version}</artifactId>
<version>${version}</version>
</dependency>if with springdoc-openapi 2.x, add:
<dependency>
<groupId>za.co.absa</groupId>
<artifactId>springdoc-openapi-scala-2_${scala_binary_version}</artifactId>
<version>${version}</version>
</dependency>where scala_binary_version is either 2.12 or 2.13.
For springdoc-openapi 1.x
import io.swagger.v3.oas.models.OpenAPI
import io.swagger.v3.oas.models.info.Info
import org.springdoc.core.customizers.OpenApiCustomiser
import org.springframework.context.annotation.{Bean, Configuration}
import za.co.absa.springdocopenapiscala.{Bundle, OpenAPIModelRegistration}
@Configuration
class OpenAPIConfiguration {
private val springDocOpenAPIScalaBundle = new Bundle(
Seq((openAPI: OpenAPI) =>
openAPI.setInfo(
new Info()
.title("Example API with springdoc-openapi v1.x")
.version("1.0.0")
)
)
)
@Bean
def openAPICustomizer: OpenApiCustomiser = springDocOpenAPIScalaBundle.customizer
@Bean
def openAPIModelRegistration: OpenAPIModelRegistration = springDocOpenAPIScalaBundle.modelRegistration
}For springdoc-openapi 2.x
import io.swagger.v3.oas.models.OpenAPI
import io.swagger.v3.oas.models.info.Info
import org.springdoc.core.customizers.OpenApiCustomizer
import org.springframework.context.annotation.{Bean, Configuration}
import za.co.absa.springdocopenapiscala.{Bundle, OpenAPIModelRegistration}
@Configuration
class OpenAPIConfiguration {
private val springDocOpenAPIScalaBundle = new Bundle(
Seq((openAPI: OpenAPI) =>
openAPI.setInfo(
new Info()
.title("Example API with springdoc-openapi v2.x")
.version("1.0.0")
)
)
)
@Bean
def openAPICustomizer: OpenApiCustomizer = springDocOpenAPIScalaBundle.customizer
@Bean
def openAPIModelRegistration: OpenAPIModelRegistration = springDocOpenAPIScalaBundle.modelRegistration
}Example model:
case class ExampleModelRequest(a: Int, b: String, c: Option[Int])
case class ExampleModelResponse(d: Seq[Int], e: Boolean)can be registered for example in Controller:
@RestController
@RequestMapping(
value = Array("/api/v1/example")
)
class ExampleController @Autowired()(openAPIModelRegistration: OpenAPIModelRegistration) {
openAPIModelRegistration.register[ExampleModelRequest]()
openAPIModelRegistration.register[ExampleModelResponse]()
@PostMapping(
value = Array("/some-endpoint"),
produces = Array(MediaType.APPLICATION_JSON_VALUE)
)
def someEndpoint(@RequestBody body: ExampleModelRequest): CompletableFuture[ExampleModelResponse] = {
...
}
}To add support for custom types (or overwrite handling of any type supported by the library)
one can create a custom ExtraTypesHandler and provide it when creating a Bundle.
There are multiple ways to do so, the simplest is to use ExtraTypesHandling.simpleMapping, for example:
OpenAPIModelRegistration.ExtraTypesHandling.simpleMapping {
case t if t =:= typeOf[JsValue] =>
val schema = new Schema
schema.setType("string")
schema.setFormat("json")
schema
}This ExtraTypesHandler handles JsValue by mapping it to simple OpenAPI string type with json format.
But ExtraTypesHandler can also be much more powerful, for example:
case class CustomClassComplexChild(a: Option[Int])
class CustomClass(val complexChild: CustomClassComplexChild) {
// these won't be included
val meaningOfLife: Int = 42
val alphabetHead: String = "abc"
}
...
val extraTypesHandler: ExtraTypesHandler = (tpe: Type) =>
tpe match {
case t if t =:= typeOf[CustomClass] =>
val childTypesToBeResolvedByTheLibrary = Set(typeOf[CustomClassComplexChild])
val handleFn: HandleFn = (resolvedChildTypes, context) => {
val name = "CustomClass"
val customClassComplexChildResolvedSchema = resolvedChildTypes(typeOf[CustomClassComplexChild])
val schema = new Schema
schema.addProperty("complexChild", customClassComplexChildResolvedSchema)
context.components.addSchemas(name, schema)
val schemaReference = new Schema
schemaReference.set$ref(s"#/components/schemas/$name")
schemaReference
}
(childTypesToBeResolvedByTheLibrary, handleFn)
}This ExtraTypesHandler handles CustomClass.
CustomClass uses CustomClassComplexChild,
so the handler requests the library to resolve its type (childTypesToBeResolvedByTheLibrary).
This resolved type is available as input to HandleFn.
Then, in handleFn, the handler creates a Schema object for CustomClass,
adds it to Components so that it can be referenced by name CustomClass,
and returns reference to that object.
It is possible to further customize registration by providing custom RegistrationConfig to OpenAPIModelRegistration.
val components = ...
val registration = OpenAPIModelRegistration(
components,
config = RegistrationConfig(
OpenAPIModelRegistration.RegistrationConfig(
sumADTsShape =
// default values apply for discriminatorPropertyNameFn, addDiscriminatorPropertyOnlyToDirectChildren
OpenAPIModelRegistration.RegistrationConfig.SumADTsShape.WithDiscriminator()
)
)
)This config property sets how sum ADTs are registered. It has two possible values:
RegistrationConfig.SumADTsShape.WithoutDiscriminator- default option, doesn't add discriminatorsRegistrationConfig.SumADTsShape.WithDiscriminator(discriminatorPropertyNameFn, addDiscriminatorPropertyOnlyToDirectChildren)- adds discriminator to sealed types schema, and also adds discriminator to sum ADTs elements properties; discriminator property name is customizable bydiscriminatorPropertyNameFn, by default it takes sealed type name, converts its first letter to lower case, and adds"Type"suffix, for example if sealed type name isExpression, the property name isexpressionType; ifaddDiscriminatorPropertyOnlyToDirectChildrenisfalse, discriminator property is added to all children, so for example inADT = A | B | C; B = D | Ediscriminator ofADTwould be added toA,C,D,E(DandEwould have discriminator ofBin addition to that) while withaddDiscriminatorPropertyOnlyToDirectChildrenset totrue(default) it would be added only toAandC
Can be found in this repo: link. It generates the following OpenAPI JSON doc:
{
"openapi": "3.0.1",
"info": {
"title": "Example API with springdoc-openapi v1.x",
"version": "1.0.0"
},
"servers": [
{
"url": "http://localhost:8080",
"description": "Generated server url"
}
],
"paths": {
"/api/v1/example/some-endpoint": {
"post": {
"tags": [
"example-controller"
],
"operationId": "someEndpoint",
"requestBody": {
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/ExampleModelRequest"
}
}
},
"required": true
},
"responses": {
"200": {
"description": "OK",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/ExampleModelResponse"
}
}
}
}
}
}
}
},
"components": {
"schemas": {
"IntLiteral": {
"required": [
"value"
],
"properties": {
"value": {
"type": "string"
}
}
},
"StringLiteral": {
"required": [
"value"
],
"properties": {
"value": {
"type": "string"
}
}
},
"Literal": {
"oneOf": [
{
"$ref": "#/components/schemas/IntLiteral"
},
{
"$ref": "#/components/schemas/StringLiteral"
}
]
},
"Something": {},
"Expression": {
"oneOf": [
{
"$ref": "#/components/schemas/Literal"
},
{
"$ref": "#/components/schemas/Something"
}
]
},
"ExampleModelRequest": {
"required": [
"a",
"b",
"d",
"e"
],
"properties": {
"a": {
"type": "integer",
"format": "int32"
},
"b": {
"type": "string"
},
"c": {
"type": "integer",
"format": "int32"
},
"d": {
"type": "string",
"format": "json"
},
"e": {
"$ref": "#/components/schemas/Expression"
},
"f": {
"$ref": "#/components/schemas/Expression"
}
}
},
"ExampleModelResponse": {
"required": [
"d",
"e"
],
"properties": {
"d": {
"type": "array",
"items": {
"type": "integer",
"format": "int32"
}
},
"e": {
"type": "boolean"
}
}
}
}
}
}Can be found in this repo: link. It generates the following OpenAPI JSON doc:
{
"openapi": "3.0.1",
"info": {
"title": "Example API with springdoc-openapi v2.x",
"version": "1.0.0"
},
"servers": [
{
"url": "http://localhost:8080",
"description": "Generated server url"
}
],
"paths": {
"/api/v1/example/some-endpoint": {
"post": {
"tags": [
"example-controller"
],
"operationId": "someEndpoint",
"requestBody": {
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/ExampleModelRequest"
}
}
},
"required": true
},
"responses": {
"200": {
"description": "OK",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/ExampleModelResponse"
}
}
}
}
}
}
},
"/api/v1/example/empty-endpoint": {
"post": {
"tags": [
"example-controller"
],
"operationId": "emptyEndpoint",
"responses": {
"204": {
"description": "No Content"
}
}
}
}
},
"components": {
"schemas": {
"ExampleModelRequest": {
"required": [
"a",
"b",
"d",
"e"
],
"properties": {
"a": {
"type": "integer",
"format": "int32"
},
"b": {
"type": "string"
},
"c": {
"type": "integer",
"format": "int32"
},
"d": {
"type": "string",
"enum": [
"OptionC",
"OptionB",
"OptionA"
]
},
"e": {
"type": "string",
"format": "json"
}
}
},
"ExampleModelResponse": {
"required": [
"d",
"e"
],
"properties": {
"d": {
"type": "array",
"items": {
"type": "integer",
"format": "int32"
}
},
"e": {
"type": "boolean"
}
}
}
}
}
}