A friendly kotlin library to validate API endpoints using an OpenApi 3 or Swagger 2 specification. Great with webflux functional. It works happily with any JVM language including Java >=8.
Supports specifications in YAML and JSON
See this complete Spring 5 Webflux example that uses openapi-spring-webflux-validator.
Java 8 or greater
<dependency>
<groupId>io.github.cdimascio</groupId>
<artifactId>openapi-spring-webflux-validator</artifactId>
<version>3.4.0</version>
</dependency>
compile 'io.github.cdimascio:openapi-spring-webflux-validator:3.4.0'
For sbt, grape, ivy and more, see here
This section and the next describe usage with Kotlin and Java respectively.
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
data class MyError(val id: String, val messages: List<String>)
val validate = Validate.configure("static/api.json") { status, messages ->
Error(status.name, messages)
}
with custom ObjectMapper factory:
val validate = Validate.configure(
openApiSwaggerPath = "api.yaml",
errorHandler = { status, message -> ValidationError(status.value(), message[0]) },
objectMapperFactory = { ObjectMapper()
.registerKotlinModule()
.registerModule(JavaTimeModule())
.configure(SerializationFeature.WRITE_DATES_AS_TIMESTAMPS, false) }
)
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}"))
}
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}")
}
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
class MyError {
private String id;
private String messages;
public MyError(String id, List<String> messages) {
this.id = id;
this.messages = messages;
}
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", (status, messages) ->
new MyError(status.getName(), messages)
);
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())
);
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
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)
}
}
}
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!