/openapi-spring-webflux-validator

🌱 A friendly kotlin library to validate API endpoints using an OpenApi 3.0 and Swagger 2.0 specification

Primary LanguageKotlinOtherNOASSERTION

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 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.

Prequisites

Java 8 or greater

Install

Maven

<dependency>
    <groupId>io.github.cdimascio</groupId>
    <artifactId>openapi-spring-webflux-validator</artifactId>
    <version>3.4.0</version>
</dependency>

Gradle

compile 'io.github.cdimascio:openapi-spring-webflux-validator:3.4.0'

For sbt, grape, ivy and more, see here

Usage (Kotlin)

This section and the next describe usage with Kotlin and Java respectively.

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

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) }
)

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 (Java 8 or greater)

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

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)
);

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

Apache 2.0

Buy Me A Coffee

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!