openapi-spring-webflux-validator
December 28, 2022 · View on GitHub
A friendly kotlin library to validate API endpoints using an OpenApi 3 or Swagger 2 specification. Great with webflux functional. It works happily with Spring Webflux 6's baseline of Jakarta JVM runtime >=17.
Supports specifications in YAML and JSON
Prequisites
and Spring Webflux 6 + Java >=17
For use with Spring Boot 2 and Webflux 5, use openapi-spring-webflux-validator version 3.5.0. Java 8 or greater is required.
Install
Maven
<dependency>
<groupId>io.github.cdimascio</groupId>
<artifactId>openapi-spring-webflux-validator</artifactId>
<version>4.2.0</version>
</dependency>
Gradle
compile 'io.github.cdimascio:openapi-spring-webflux-validator:4.2.0'
For sbt, grape, ivy and more, see here
Usage (Kotlin)
This section and the next describe usage with Kotlin and Java respectively.
See this complete Spring Webflux example that uses openapi-spring-webflux-validator.
Configure (Kotlin)
This one-time configuration requires you to provide the location of the openapi/swagger specification and an optional custom error handler.
Supports JSON and YAML
import io.github.cdimascio.openapi.Validate
val validate = Validate.configure("static/api.yaml")
with custom error handler
import org.springframework.web.reactive.function.server.ServerRequest
data class MyError(val request: ServerRequest, val code: String, val messages: List<String>)
val validate = Validate.configure("static/api.json") { request, status, messages ->
MyError(request, status.name, messages)
}
with custom ObjectMapper factory:
val validate = Validate.configure(
openApiSwaggerPath = "api.yaml",
errorHandler = { request, status, message -> ValidationError(request, status.value(), message[0]) },
objectMapperFactory = { ObjectMapper()
.registerKotlinModule()
.registerModule(JavaTimeModule())
.configure(SerializationFeature.WRITE_DATES_AS_TIMESTAMPS, false) }
)
Validate a request (Kotlin + Reactor)
You can now validate a request in a coroutine style,
using the validate instance created above:
without a body
validate.request(req) {
// Do stuff e.g. return a list of names
ok().body(Mono.just(listOf("carmine", "alex", "eliana")))
}
with body
validate.request(req).withBody(User::class.java) { body ->
// Note that body is deserialized as User!
// Now you can do stuff.
// For example, lets echo the request as the response
ok().body(Mono.just(body))
}
with body you want to process as string (e.g. for computing a request signature), or that you want to deserialize somehow specifically
val identity: (String) -> String = { it }
validate.request(req).withBody(String::class.java, readValue = identity) { body ->
ok().body(Mono.just("content length is ${body.length}"))
}
Validate a request (Kotlin + coroutines)
Or you can validate a request in a coroutine style,
using the validate instance created above:
without a body
validate.requestAndAwait(req) {
// Do stuff e.g. return a list of names
ok().bodyValueAndAwait(listOf("carmine", "alex", "eliana"))
}
with body
validate.request(req).awaitBody(User::class.java) { body: User ->
// Note that body is deserialized as User!
// Now you can do stuff.
// For example, lets echo the request as the response
ok().bodyValueAndAwait(body)
}
with body you want to process as string (e.g. for computing a request signature), or that you want to deserialize somehow specifically
val identity: (String) -> String = { it }
validate.request(req).awaitBody(String::class.java, identity) { body: String ->
ok().bodyValueAndAwait("content length is ${body.length}")
}
Usage
Configure (Java)
This one-time configuration requires you to provide the location of the openapi/swagger specification and an optional custom error handler.
import io.github.cdimascio.openapi.Validate;
Validate<ValidationError> validate = Validate.configure("static/api.json")
with custom error handler
import org.springframework.web.reactive.function.server.ServerRequest;
class MyError {
private ServerRequest request;
private String id;
private String messages;
public MyError(ServerRequest request, String id, List<String> messages) {
this.request = request;
this.id = id;
this.messages = messages;
}
public ServerRequest getRequest() {
return request;
}
public String getId() {
return id;
}
public void setId(String id) {
this.id = id;
}
public List<String> getMessages() {
return messages;
}
public void setMessages(List<String> messages) {
this.messages = messages;
}
}
Validate<ValidationError> validate = Validate.configure("static/api.json", (request, status, messages) ->
new MyError(request, status.getName(), messages)
);
Validate a request (Java)
Using the validate instance created above, you can now validate a request:
without a body
ArrayList<String> users = new ArrayList<String>() {{
add("carmine");
add("alex");
add("eliana");
}};
validate.request(req, () ->
// Do stuff e.g. return a list of user names
ServerResponse.ok().bodyValue(users)
);
with body
validate
.request(req)
.withBody(User.class, user ->
// Note that body is deserialized as User!
// Now you can do stuff.
// For example, lets echo the request as the response
ServerResponse.ok().bodyValue(user)
);
with body you want to process as string (e.g. for computing a request signature)
validate
.request(req)
.withBody(String.class, s -> s, body ->
ServerResponse.ok().bodyValue("content length is " + body.length())
);
Example Validation Output
Let's assume a POST request to create a user requires the following request body:
{
"firstname": "carmine",
"lastname": "dimasico"
}
Let's now assume an API user misspells lastname as lastnam
curl -X POST http://localhost:8080/api/users -H "Content-Type: application/json" -d'{
"firstname": "c",
"lastnam": "d"
}'
openapi-spring-webflux-validator automatically validates the request against a Swagger spect and returns:
{
"code": 400,
"messages":[
"Object instance has properties which are not allowed by the schema: [\"lastnam\"]",
"Object has missing required properties ([\"lastname\"])"
]
}
Woah! Cool!! :-D
Example
Let's say you have an endpoint /users that supports both GET and POST operations.
You can create those routes and validate them like so:
Create the routes in a reactive or coroutine style:
package myproject.controllers
import org.springframework.core.io.ClassPathResource
import org.springframework.http.MediaType.*
import org.springframework.web.reactive.function.server.ServerResponse.permanentRedirect
import org.springframework.web.reactive.function.server.coRouter
import org.springframework.web.reactive.function.server.plus
import org.springframework.web.reactive.function.server.router
import java.net.URI
class Routes(private val userHandler: UserHandler) {
fun router() = router {
"/api".nest {
accept(APPLICATION_JSON).nest {
POST("/users", userHandler::create)
}
accept(TEXT_EVENT_STREAM).nest {
GET("/users", userHandler::findAll)
}
}
} + coRouter {
"/coApi".nest {
accept(APPLICATION_JSON).nest {
POST("/users", userHandler::coCreate)
}
accept(TEXT_EVENT_STREAM).nest {
GET("/users", userHandler::coFindAll)
}
}
}
}
package myproject
import io.github.cdimascio.openapi.Validate
val validate = Validate.configure("static/api.yaml")
Validate with openapi-spring-webflux-validator
package myproject.controllers
import myproject.models.User
import myproject.validate
import org.springframework.web.reactive.function.server.ServerRequest
import org.springframework.web.reactive.function.server.ServerResponse
import org.springframework.web.reactive.function.server.ServerResponse.ok
import org.springframework.web.reactive.function.server.bodyValueAndAwait
import reactor.core.publisher.Flux
import reactor.core.publisher.Mono
class UserHandler {
fun findAll(req: ServerRequest): Mono<ServerResponse> {
return validate.request(req) {
ok().bodyValue(listOf("carmine", "alex", "eliana"))
}
}
fun create(req: ServerRequest): Mono<ServerResponse> {
return validate.request(req).withBody(User::class.java) {
// it is the request body deserialized as User
ok().bodyValue(it)
}
}
suspend fun coFindAll(req: ServerRequest): ServerResponse {
return validate.requestAndAwait(req) {
ok().bodyValueAndAwait(listOf("carmine", "alex", "eliana"))
}
}
suspend fun coCreate(req: ServerRequest): ServerResponse {
return validate.request(req).awaitBody(User::class.java) {
// it is the request body deserialized as User
ok().bodyValueAndAwait(it)
}
}
}
License
Contributors ✨
Thanks goes to these wonderful people (emoji key):
Carmine DiMascio 💻 ⚠️ 📖 |
Krzysiek Kruczyński 💻 ⚠️ 📖 |
Chejerla Karthik 💻 |
Katie Levy 💻 |
Ilya Builuk 💻 |
Simon Zambrovski 💻 ⚠️ |
This project follows the all-contributors specification. Contributions of any kind welcome!
