OpenAPI generator to spring-boot with custom java validations

Matúš Bartko
9 min readJan 5, 2021

--

This tutorial will show you how to add capability of using custom constraint validation annotations in code generated from OpenAPI specs.

If you need to add custom validation to your existing project with OpenAPI generator already setup, you can skip intro and go straight to the tutorial section.

Code for this story is available on https://github.com/Matusko/open-api-custom-validations.git

Intro

I am using OpenAPI as universal way to define specification of REST interfaces. This story is not about general benefits of using OpenAPI (internet is full of it, you can start here). I am going to focus on one specific problem I encountered when dealing with OpenAPI generator.

OpenAPI generator enable you to generate code from OpenAPI specs. It can generate both consumers (clients) and producers (servers) of REST services written in multiple languages and libraries.

I am writing server side in spring-boot framework so in my scenario I wanted to generate java interfaces with spring annotations defining REST Services. These interfaces I can then implement as spring controllers and connect them to datastores etc.

Basically I want this (petstore.yaml):

to became this generated code (PetsApi.java):

so then I can implement it in my project as this (PetsController.java):

Notice that since I defined limit parameter as required in petstore.yaml. My generated code PetsApi.java does have @NotNull annotation over limit method parameter. That means that java throws Exception when limit query parameter is not send in HTTP request. There are some more validation properties such as maxLength, minLength, pattern etc. (full list with details can be found here) that transform into java validation constraints. But what if I want to add my custom very specific validation unknown to OpenAPI specs? Same question was asked on stackoverflow but remains without answer for over a year.

I think of some ways of doing it:

  1. You can add some another validation layer in your code, which is independent of OpenAPI generator. This layer will be called from PetsController and PetsController will validate only basic OpenAPI known constraints.
  2. You can add you validations not via annotations, but via xml config as shown here.
  3. maybe something else.
  4. Hack it a bit. I was looking for a solution in which my custom validation will be defined in OpenAPI spec same way as “required”. Naturally I decided not to use solutions 1 or 2 (even thought it might be the right way for a lot of cases). I found out the openapi-generator actually provides a way of modifying the way the code is generated. That means that I can actually define custom constraint in OpenAPI specs as my own made up properties.

Following tutorial covers solution 4.

Tutorial

Prerequisites

  • Basic knowledge of Java, maven, spring-boot and OpenAPI
  • Installed Java 11 or higher (I used Java 11 but should work on also on Java 8)
  • Installed maven 3.x.x (or you can use mvnw in project root)
  • Installed git (if you want to checkout code for each chapter)

Chapters

git clone https://github.com/Matusko/open-api-custom-validations.git
git checkout chapter_you_want_to_go_to
  1. Init Spring Project
    git checkout chapter1
  2. Add openapi-generator-maven-plugin
    git checkout chapter2
  3. Add custom validation constraint to query parameter
    git checkout chapter3
  4. Add custom validation constraint to POST request body (json) field
    git checkout chapter4

1. Init Spring Project

Let’s go to spring initializr and generate our spring-boot app with settings and dependencies as shown on image below.
Spring Web and Validation dependencies are necessary.

You can verify if everything works by running from project root:

mvn spring-boot:run

You should see in logs something like:

2020-12-31 00:23:41.693  INFO 26508 --- [  restartedMain] o.s.b.w.embedded.tomcat.TomcatWebServer  : Tomcat started on port(s): 8080 (http) with context path ''
2020-12-31 00:23:41.701 INFO 26508 --- [ restartedMain] .t.o.OpenapicustomvalidationsApplication : Started OpenapicustomvalidationsApplication in 1.49 seconds (JVM running for 1.787)

2. Add openapi-generator-maven-plugin

Step 1
At first lets create our OpenAPI v3 specs. Since I was lazy to create my own I used petstore (well known in openapi community). So I downloaded and saved it as {project_root}/src/main/resources/openapi/specs/petstore.yaml

Step 2
Then since I want to focus on validations and made the limit parameter required in following section:

{project_root}/src/main/resources/openapi/specs/petstore.yaml

Step 3
Add following dependencies to pom.xml

{project_root}/pom.xml

Step 4
Add openapi-generator-maven-plugin to pom.xml

{project_root}/pom.xml

Lot of important stuff in previous few lines:

  1. I have defined the location of petstore.yaml
  2. I have specified that I want to generate spring code
  3. In configOptions I specified that I want to generate only interfaces, not whole controllers
  4. And also java package names

At this point you can execute

mvn clean install

and you should be able to see generated model classes Pet.java and Error.java in {project_root}/target/generated-sources/openapi/src/main/java/sk/matusko/tutorial/openapicustomvalidations/model as well as our PetsApi.java interface in {project_root}/target/generated-sources/openapi/src/main/java/sk/matusko/tutorial/openapicustomvalidations/api

Step 5
Write your controller PetsController.java

{project_root}/src/main/java/sk/matusko/tutorial/openapicustomvalidations/rest/PetsController.java

I have implemented only 2/3 methods since that is all we need for purposes of this tutorial.

Now you can execute

mvn spring-boot:run

Test REST
And the use some http client (curl, postman, IntelliJ IDEA, etc.) to test our REST api. I am using IntelliJ IDEA.

{project_root}/misc/createPet.http returned HTTP status code 201 as we implemented it

POST http://localhost:8080/pets
Content-Type: application/json
#resultPOST http://localhost:8080/petsHTTP/1.1 201
Content-Length: 0
Date: Thu, 31 Dec 2020 00:10:31 GMT
Keep-Alive: timeout=60
Connection: keep-alive
<Response body is empty>Response code: 201; Time: 56ms; Content length: 0 bytes

{project_root}/misc/getPet.http returned HTTP status code 501 because we did not implemented it

GET http://localhost:8080/pets/8#resultGET http://localhost:8080/pets/8HTTP/1.1 501 
Content-Length: 0
Date: Thu, 31 Dec 2020 00:15:33 GMT
Connection: close
<Response body is empty>Response code: 501; Time: 54ms; Content length: 0 bytes

and {project_root}/misc/listPets.http returned HTTP status code 200 when we sent limit query parameter

GET http://localhost:8080/pets?limit=7#resultGET http://localhost:8080/pets?limit=7HTTP/1.1 200 
Content-Type: application/json
Transfer-Encoding: chunked
Date: Thu, 31 Dec 2020 00:17:42 GMT
Keep-Alive: timeout=60
Connection: keep-alive
[
{
"id": 1,
"name": "Bobo",
"tag": null
}
]
Response code: 200; Time: 45ms; Content length: 35 bytes

and HTTP status code 400 when query parameter limit is not sent

GET http://localhost:8080/pets#resultGET http://localhost:8080/petsHTTP/1.1 400 
Content-Type: application/json
Transfer-Encoding: chunked
Date: Thu, 31 Dec 2020 00:18:59 GMT
Connection: close
{
"timestamp": "2020-12-31T00:18:59.017+00:00",
"status": 400,
"error": "Bad Request",
"trace": "org.springframework.web.bind.MissingServletRequestParameterException: Required Integer parameter 'limit' is not present\n\tat org.springframework.web.method.annotation.RequestParamMethodArgumentResolver.handleMissingValue(RequestParamMethodArgumentResolver.java:....... ",
"message": "Required Integer parameter 'limit' is not present",
"path": "/pets"
}
Response code: 400; Time: 47ms; Content length: 5213 bytes

3. Add custom validation constraint to query parameter

Step 1
Let’s create our custom contraint validator. For purposes of this I created validator which validates if parameter of type Long is even number. i want query parameter “limit” to be allowed only when it is even.

{project_root}/src/main/java/sk/matusko/tutorial/openapicustomvalidations/validators/EvenLong.java
{project_root}/src/main/java/sk/matusko/tutorial/openapicustomvalidations/validators/EvenLongValidator.java

Step 2
Lets edit petstore.yaml so that limit parameter is long and not integer. Otherwise our validation will fail because I did not made it for Integer data type. You can do this by changing format to int64

{project_root}/src/main/resources/openapi/specs/petstore.yaml

Step 3
Fix {project_root}/src/main/java/sk/matusko/tutorial/openapicustomvalidations/rest/PetsController.java because you changed limit data type from integer to long

Step 4
Add new custom field for our custom validations. OpenAPI specification is strict but it provides a way of adding custom fields into our yaml. It can be of any type and field name have to match regex ^x- so it basically must start with “x-”.

Since I am lazy, this time I put my whole annotation into specs same way I want for them to appear in my generated java. But feel free to add nicer approach and maybe created more advanced logic, so that you don’t write ugly java meta code in OpenAPI specs. I created x-constraints field.

{project_root}/src/main/resources/openapi/specs/petstore.yaml

Step 5
For generating of code from OpenAPI specs openapi-generator-maven-plugin is using templating/rendering engine mustache. The ecosystem of mustache templates can be customized my providing overridden custom templates.
Templates for various frameworks which you can override are here.

Create {project_root}/src/main/resources/openapi/templates/beanValidationCore.mustache and copy content of beanValidationCore.mustache to it.
Then add line

{{ vendorExtensions.x-constraints }}

at top of the file so it looks like this:

{project_root}/src/main/resources/openapi/templates/beanValidationCore.mustache

Now our annotation from petstore.yaml will be rendered with other constraint annotations. Since we haven’t define import from our custom validators package, compile phase will fail.

Step 6
Define import of our validation annotation. Create
{project_root}/src/main/resources/openapi/templates/api.mustache and copy content from api.mustache to it.
Then add line

import sk.matusko.tutorial.openapicustomvalidations.validators.*;

so it looks like this:

{project_root}/src/main/resources/openapi/templates/api.mustache

Step 7
We have to tell openapi-generator-maven-plugin to use mustache templates from our folder.
This can be done by specifying templateDirectory property in pom.xml.

{project_root}/pom.xml

Let`s execute:

mvn clean install
mvn spring-boot:run

Test REST
Now we can see that when we call GET http://localhost:8080/pets with even limit parameter we got HTTP status code 200 but when limit is odd number we got HTTP status 500,

The reason why status code is 500 and not 400 is because we have not defined ExceptionHandler for ConstraintViolationException thrown by violated constraint EvenLong. HTTP status code 500 is default for unhandled exceptions.

{project_root}/misc/listPets.http when limit is odd number:

GET http://localhost:8080/pets?limit=7#resultGET http://localhost:8080/pets?limit=7HTTP/1.1 500 
Content-Type: application/json
Transfer-Encoding: chunked
Date: Thu, 31 Dec 2020 16:42:27 GMT
Connection: close
{
"timestamp": "2020-12-31T16:42:27.878+00:00",
"status": 500,
"error": "Internal Server Error",
"trace": "javax.validation.ConstraintViolationException: listPets.limit: Not an even number\n\tat org.springframework.validation.beanvalidation.MethodValidationInterc.....",
"message": "listPets.limit: Not an even number",
"path": "/pets"
}
Response code: 500; Time: 51ms; Content length: 5652 bytes

and when limit is even number:

GET http://localhost:8080/pets?limit=6#resultGET http://localhost:8080/pets?limit=6HTTP/1.1 200 
Content-Type: application/json
Transfer-Encoding: chunked
Date: Thu, 31 Dec 2020 16:47:59 GMT
Keep-Alive: timeout=60
Connection: keep-alive
[
{
"id": 1,
"name": "Bobo",
"tag": null
}
]
Response code: 200; Time: 36ms; Content length: 35 bytes

4. Add custom validation constraint to POST request body (json) field

Last thing I want to share with you is how to add custom constraint validation over field in json HTTP request body.

Step 1
At first we have to define request body in createPets operation in petstore.yaml so add requestBody object to following part:

{project_root}/src/main/resources/openapi/specs/petstore.yaml

Now when you run

mvn install

it fails, because signature of method createPet in generated PetApi.java interface have changed so you have to reflect it in PetsController.java.

{project_root}/src/main/java/sk/matusko/tutorial/openapicustomvalidations/rest/PetsController.java

Step 2
Define x-constraints attribute in id property of Pet component in petstore.yaml

{project_root}/src/main/resources/openapi/specs/petstore.yaml

Step 3
And now the last thing. The template {project_root}/src/main/resources/openapi/templates/beanValidationCore.mustache is common for rendering constraint annotations over both model attributes and query parameters. But model and api end up in 2 sepearate .java files. That means that java import we previously defined for api file should be also added to mustache template for rendering model. Create {project_root}/src/main/resources/openapi/templates/model.mustache and copy content from model.mustache to it.

Then add import

import sk.matusko.tutorial.openapicustomvalidations.validators.*;

so it looks like this:

{project_root}/src/main/resources/openapi/templates/model.mustache

Test REST
Now we can see results our effort {project_root}/misc/createPet.http. You can see that when id attribute is odd, HTTP status 400 is returned because our constraint was violated. The reason behind different status codes for constraint violation of query parameter and json field is that different Exceptions are thrown. Spring then assign different status codes to them, but that can be customized.

POST http://localhost:8080/pets
Content-Type: application/json

{
"id":3,
"name":"Bobo",
"tag":null
}
#resultPOST http://localhost:8080/petsHTTP/1.1 400
Content-Type: application/json
Transfer-Encoding: chunked
Date: Thu, 31 Dec 2020 17:14:54 GMT
Connection: close
{
"timestamp": "2020-12-31T17:14:54.167+00:00",
"status": 400,
"error": "Bad Request",
"trace": "org.springframework.web.bind.MethodArgumentNotValidException: Validation failed for argument [0] in public org.springframework.http.ResponseEntity<java.lang.Void> ....",
"message": "Validation failed for object='pet'. Error count: 1",
"errors": [
{
"codes": [
"EvenLong.pet.id",
"EvenLong.id",
"EvenLong.java.lang.Long",
"EvenLong"
],
"arguments": [
{
"codes": [
"pet.id",
"id"
],
"arguments": null,
"defaultMessage": "id",
"code": "id"
}
],
"defaultMessage": "Not an even number",
"objectName": "pet",
"field": "id",
"rejectedValue": 3,
"bindingFailure": false,
"code": "EvenLong"
}
],
"path": "/pets"
}
Response code: 400; Time: 39ms; Content length: 5864 bytes

But if id attribute is even constraint is not violated so HTTP status code 201 is returned.

POST http://localhost:8080/pets
Content-Type: application/json

{
"id":180,
"name":"Bobo",
"tag":null
}
#resultPOST http://localhost:8080/petsHTTP/1.1 201
Content-Length: 0
Date: Thu, 31 Dec 2020 17:21:20 GMT
Keep-Alive: timeout=60
Connection: keep-alive
<Response body is empty>Response code: 201; Time: 33ms; Content length: 0 bytes

That`s all folks. Hope you enjoyed it!

--

--