cdimascio / openapi-spring-webflux-validator

🌱 A friendly kotlin library to validate API endpoints using an OpenApi 3.0 and Swagger 2.0 specification
Other
97 stars 13 forks source link
functional hacktoberfest java openapi openapi3 rest spring swagger swagger-validator validation webflux

openapi-spring-webflux-validator

Maven Central Codacy Badge All Contributors

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 ```xml io.github.cdimascio openapi-spring-webflux-validator 4.2.0 ``` ### Gradle ```groovy compile 'io.github.cdimascio:openapi-spring-webflux-validator:4.2.0' ``` For sbt, grape, ivy and more, see [here](https://search.maven.org/#artifactdetails%7Cio.github.cdimascio%7Copenapi-spring-webflux-validator%7C2.0.0%7Cjar) ## 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](https://github.com/cdimascio/kotlin-swagger-spring-functional-template). ### 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` ```kotlin import io.github.cdimascio.openapi.Validate val validate = Validate.configure("static/api.yaml") ``` with custom error handler ```kotlin import org.springframework.web.reactive.function.server.ServerRequest data class MyError(val request: ServerRequest, val code: String, val messages: List) val validate = Validate.configure("static/api.json") { request, status, messages -> MyError(request, status.name, messages) } ``` with custom ObjectMapper factory: ```kotlin 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](#configure-kotlin): without a body ```kotlin validate.request(req) { // Do stuff e.g. return a list of names ok().body(Mono.just(listOf("carmine", "alex", "eliana"))) } ``` with body ```kotlin 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 ```kotlin 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](#configure-kotlin): without a body ```kotlin validate.requestAndAwait(req) { // Do stuff e.g. return a list of names ok().bodyValueAndAwait(listOf("carmine", "alex", "eliana")) } ``` with body ```kotlin 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 ```kotlin 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_. ```java import io.github.cdimascio.openapi.Validate; Validate validate = Validate.configure("static/api.json") ``` with custom error handler ```java 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 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 getMessages() { return messages; } public void setMessages(List messages) { this.messages = messages; } } ``` ```java Validate 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 ```java ArrayList users = new ArrayList() {{ 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 ```java 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) ```java 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: ```json { "firstname": "carmine", "lastname": "dimasico" } ``` Let's now assume an API user misspells `lastname` as `lastnam` ```shell 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: ```json { "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:** ```kotlin 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) } } } } ``` ```kotlin package myproject import io.github.cdimascio.openapi.Validate val validate = Validate.configure("static/api.yaml") ``` **Validate with openapi-spring-webflux-validator** ```kotlin 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 { return validate.request(req) { ok().bodyValue(listOf("carmine", "alex", "eliana")) } } fun create(req: ServerRequest): Mono { 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 [Apache 2.0](https://www.apache.org/licenses/LICENSE-2.0) Buy Me A Coffee ## Contributors ✨ Thanks goes to these wonderful people ([emoji key](https://allcontributors.org/docs/en/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!