DZone
Thanks for visiting DZone today,
Edit Profile
  • Manage Email Subscriptions
  • How to Post to DZone
  • Article Submission Guidelines
Sign Out View Profile
  • Post an Article
  • Manage My Drafts
Over 2 million developers have joined DZone.
Log In / Join
Please enter at least three characters to search
Refcards Trend Reports
Events Video Library
Refcards
Trend Reports

Events

View Events Video Library

Zones

Culture and Methodologies Agile Career Development Methodologies Team Management
Data Engineering AI/ML Big Data Data Databases IoT
Software Design and Architecture Cloud Architecture Containers Integration Microservices Performance Security
Coding Frameworks Java JavaScript Languages Tools
Testing, Deployment, and Maintenance Deployment DevOps and CI/CD Maintenance Monitoring and Observability Testing, Tools, and Frameworks
Culture and Methodologies
Agile Career Development Methodologies Team Management
Data Engineering
AI/ML Big Data Data Databases IoT
Software Design and Architecture
Cloud Architecture Containers Integration Microservices Performance Security
Coding
Frameworks Java JavaScript Languages Tools
Testing, Deployment, and Maintenance
Deployment DevOps and CI/CD Maintenance Monitoring and Observability Testing, Tools, and Frameworks

The software you build is only as secure as the code that powers it. Learn how malicious code creeps into your software supply chain.

Apache Cassandra combines the benefits of major NoSQL databases to support data management needs not covered by traditional RDBMS vendors.

Generative AI has transformed nearly every industry. How can you leverage GenAI to improve your productivity and efficiency?

Modernize your data layer. Learn how to design cloud-native database architectures to meet the evolving demands of AI and GenAI workloads.

Related

  • Exploring Hazelcast With Spring Boot
  • How To Build Web Service Using Spring Boot 2.x
  • OpenAPI 3 Documentation With Spring Boot
  • Spring Microservices RESTFul API Documentation With Swagger Part 1

Trending

  • Navigating and Modernizing Legacy Codebases: A Developer's Guide to AI-Assisted Code Understanding
  • Navigating Change Management: A Guide for Engineers
  • Dropwizard vs. Micronaut: Unpacking the Best Framework for Microservices
  • Advancing Robot Vision and Control
  1. DZone
  2. Coding
  3. Frameworks
  4. Extending Swagger and Springdoc Open API

Extending Swagger and Springdoc Open API

Learn how to use Swagger schema extensions, and document additional constraints that would otherwise remain undocumented, including custom validators.

By 
Raghuraman Ramaswamy user avatar
Raghuraman Ramaswamy
DZone Core CORE ·
Updated Apr. 17, 24 · Tutorial
Likes (15)
Comment
Save
Tweet
Share
49.6K Views

Join the DZone community and get the full member experience.

Join For Free

Java adoption has shifted from version 1.8 to at least Java 17. Concurrently, Spring Boot has advanced from version 2.x to 3.2.2. The springdoc project has transitioned from the older library 'springdoc-openapi-ui' to 'springdoc-openapi-starter-webmvc-ui' for its functionality. These updates mean that readers relying on older articles may find themselves years behind in these technologies. The author has updated this article so that readers are using the latest versions and don't struggle with outdated information during migration.


In my last recent articles — OpenAPI 3 Documentation With Spring Boot and Doing More With Springdoc OpenAPI  — we tried out a Spring Boot Open API 3-enabled REST project and explored some of its capabilities, namely:

  • Automatic JSR-303-related Swagger documentation
  • How Maven builds properties could be shown as project information in the Swagger documentation
  • Rendering fully qualified names in the generated Swagger documentation
  • Global exception handling using Controller Advice and its related Swagger documentation

We also discussed how in future releases of springdoc-openapi:

  • Achieving FQNs can be a lot easier (since achieved). 
  • springdoc-openapi will handle @ControlerAdvice related documentation in an even better manner with more flexibility (since achieved).

Previously, amongst other details, we saw how we were able to leverage some of the JSR 303 annotations. We did notice some annotations were being ignored; e.g., javax.validation.constraints.Email and org.hibernate.validator.constraints.CreditCardNumber.

New Objective

Having detailed contracts specified for input and output would be very important for any API. Wouldn't it be nice if we could extend Swagger's behavior and communicate via its automated documentation the information regarding these additional annotations and also custom validation annotations?

Let's explore that. Let's keep the code as simple as possible. We will start from scratch and write enough code to achieve our objective.

We won't be repeating the exception handling and Controller Advice concepts already detailed the last time (just to keep this article's code as simple as possible).

As before, we are going to refer to Building a RESTful Web Service and springdoc-openapi v2.5.0.

I would also like to thank Badr Nass Lahsen of Springdoc for reviewing the article and code.

Prerequisites

  • Java 17.x
  • Maven 3.x
  • Lombok installed in the IDE (if using an IDE)

Steps

Start by creating a Maven JAR project. Below, you will see the pom.xml to use:

XML
 
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
		 xmlns="http://maven.apache.org/POM/4.0.0"
		 xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 https://maven.apache.org/xsd/maven-4.0.0.xsd">
	<modelVersion>4.0.0</modelVersion>
	<parent>
		<groupId>org.springframework.boot</groupId>
		<artifactId>spring-boot-starter-parent</artifactId>
		<version>3.2.2</version>
		<relativePath ></relativePath> <!-- lookup parent from repository -->
	</parent>
	<groupId>com.example</groupId>
	<artifactId>sample</artifactId>
	<version>0.0.1</version>
	<name>sample</name>
	<description>Demo project for Spring Boot with openapi 3 documentation</description>

	<properties>
		<java.version>17</java.version>
	</properties>

	<dependencies>
		<dependency>
			<groupId>org.springframework.boot</groupId>
			<artifactId>spring-boot-starter-web</artifactId>
		</dependency>
		<dependency>
			<groupId>org.springframework.boot</groupId>
			<artifactId>spring-boot-starter-validation</artifactId>
		</dependency>
		<dependency>
			<groupId>org.springdoc</groupId>
			<artifactId>springdoc-openapi-starter-webmvc-ui</artifactId>
			<version>2.5.0</version>
		</dependency>
		<dependency>
			<groupId>org.projectlombok</groupId>
			<artifactId>lombok</artifactId>
		</dependency>
		
	</dependencies>
	<build>
		<plugins>
			<plugin>
				<groupId>org.springframework.boot</groupId>
				<artifactId>spring-boot-maven-plugin</artifactId>
			</plugin>
		</plugins>
	</build>
</project>


Note the "springdoc-openapi-starter-webmvc-ui" dependency. 

If using the Eclipse IDE, we might need to do a Maven update on the project (right-click on Project  - Maven > Update Project) after creating the pom.xml with the content above.

Now, let's create a small Java bean class similar to previous articles.

Java
 
package sample.model;

import jakarta.validation.constraints.Email;
import jakarta.validation.constraints.Max;
import jakarta.validation.constraints.Min;
import jakarta.validation.constraints.NotBlank;
import jakarta.validation.constraints.NotNull;
import jakarta.validation.constraints.Pattern;
import jakarta.validation.constraints.Size;
import jakarta.xml.bind.annotation.XmlAccessType;
import jakarta.xml.bind.annotation.XmlAccessorType;
import jakarta.xml.bind.annotation.XmlRootElement;

import lombok.Data;
import org.hibernate.validator.constraints.CreditCardNumber;
import sample.customvalidations.DateTimeType;
import sample.customvalidations.LocalDateTimeFormat;

@Data
@XmlRootElement(name = "person")
@XmlAccessorType(XmlAccessType.FIELD)
public class Person {

	private long id;

	@Size(min = 2)
	private String firstName;

	@NotNull
	@NotBlank
	private String lastName;

	@Pattern(regexp = ".+@.+\\..+", message = "Please provide a valid email address")
	private String email;

	@Email()
	private String email1;

	@Min(18)
	@Max(30)
	private int age;

	@CreditCardNumber
	private String creditCardNumber;

	@LocalDateTimeFormat(pattern = "yyyyMMdd", dateTimeType = DateTimeType.Date, message = "Invalid dateTimeField Format. It Should be in yyyyMMdd format")
	private String registrationDate;

}


Person.java might complain about sample.customvalidations.DateTimeType and sample.customvalidations.LocalDateTimeFormat. We will add those classes later as we proceed with the steps.

This is an example of a Java bean. This Java bean also now contains an additional "registrationDate" field just to demonstrate custom validators. 

Now, let's create a controller.

Java
 
package sample.controller;

import jakarta.validation.Valid;

import sample.model.Person;

import org.springframework.http.MediaType;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RestController;

import io.swagger.v3.oas.annotations.media.Content;
import io.swagger.v3.oas.annotations.media.ExampleObject;

@RestController 
public class PersonController {

	@PostMapping(path = "/person", consumes = { MediaType.APPLICATION_JSON_VALUE, 
			MediaType.APPLICATION_XML_VALUE })
	@io.swagger.v3.oas.annotations.parameters.RequestBody(required = true, content = @Content(examples = {
			@ExampleObject(value = INVALID_REQUEST, name = "invalidRequest", description = "Invalid Request"),
			@ExampleObject(value = VALID_REQUEST, name = "validRequest", description = "Valid Request") }))
	public Person person(@Valid @RequestBody Person person) {
		return person;
	}
	
	private static final String VALID_REQUEST = """
			{
			  "id": 0,
			  "firstName": "string",
			  "lastName": "string",
			  "email": "abc@abc.com",
			  "email1": "abc@abc.com",
			  "age": 20,
			  "creditCardNumber": "4111111111111111",
			  "registrationDate": "20211231"
			}""";

	private static final String INVALID_REQUEST = """
			{
			  "id": 0,
			  "firstName": "string",
			  "lastName": "string",
			  "email": "abcabc.com",
			  "email1": "abcabc.com",
			  "age": 17,
			  "creditCardNumber": "411111111111111",
			  "registrationDate": "string"
			}""";
}


Let's make some entries in src\main\resources\application.properties. Please create the file accordingly.

Properties files
 
application-description=@project.description@
application-version=@project.version@
springdoc.swagger-ui.show-extensions=true
springdoc.swagger-ui.show-common-extensions=true
server.error.include-message=always
server.error.include-binding-errors=always
springdoc.use-fqn=true


The application-description and application-version entries will pass on Maven build-related information to the OpenAPI documentation.

Java
 
package sample.config;

import io.swagger.v3.oas.models.OpenAPI;
import io.swagger.v3.oas.models.info.Info;
import io.swagger.v3.oas.models.info.License;

import org.springframework.beans.factory.annotation.Value;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;

@Configuration
class OpenApiConfig {

	@Bean
	public OpenAPI customOpenAPI(@Value("${application-description}") String appDesciption, 
			@Value("${application-version}") String appVersion) {
		return new OpenAPI()
				.info(new Info()
						.title("sample application API")
						.version(appVersion)
						.description(appDesciption)
						.termsOfService("http://swagger.io/terms/")
						.license(new License().name("Apache 2.0")
								.url("http://springdoc.org")));
	}

}


Let's write the Spring Boot application class.

Java
 
package sample;

import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;

@SpringBootApplication
public class SampleApplication {

	public static void main(String[] args) {
		SpringApplication.run(SampleApplication.class, args);
	}

}


Let's add some more code to help demonstrate custom validators.

Java
 
package sample.customvalidations;

public enum DateTimeType {
	DateTime,
	Date,
	Time
}


Java
 
package sample.customvalidations;

import java.lang.annotation.Documented;
import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.Target;

import jakarta.validation.Constraint;
import jakarta.validation.Payload;

import static java.lang.annotation.RetentionPolicy.RUNTIME;

@Target({ ElementType.FIELD, ElementType.METHOD, ElementType.PARAMETER,
		ElementType.ANNOTATION_TYPE })
@Retention(RUNTIME)
@Constraint(validatedBy = LocalDateTimeValidator.class)
@Documented
public @interface LocalDateTimeFormat {

	String message() default "{message.key}";

	Class<?>[] groups() default {};

	Class<? extends Payload>[] payload() default {};

	String pattern();

	DateTimeType dateTimeType() default DateTimeType.DateTime;

}


Java
 
package sample.customvalidations;

import java.time.LocalDate;
import java.time.LocalDateTime;
import java.time.LocalTime;
import java.time.format.DateTimeFormatter;

import javax.validation.ConstraintValidator;
import javax.validation.ConstraintValidatorContext;

public class LocalDateTimeValidator implements ConstraintValidator<LocalDateTimeFormat, String> {

	private String pattern;

	private DateTimeType dateTimeType;

	@Override
	public void initialize(LocalDateTimeFormat constraintAnnotation) {
		this.pattern = constraintAnnotation.pattern();
		this.dateTimeType = constraintAnnotation.dateTimeType();
	}

	@Override
	public boolean isValid(String object, ConstraintValidatorContext constraintContext) {
		if (object == null || "".equals(object)) {
			return true;
		}

		try {
			DateTimeFormatter dateFormatter = DateTimeFormatter.ofPattern(pattern);
			if (DateTimeType.Time.equals(dateTimeType)) {
				LocalTime.parse(object, dateFormatter);
			}
			else if (DateTimeType.Date.equals(dateTimeType)) {
				LocalDate.parse(object, dateFormatter);
			}
			else {
				LocalDateTime.parse(object, dateFormatter);
			}
			return true;
		}
		catch (Exception e) {
			// e.printStackTrace();
			return false;
		}
	}
}


At this stage, this is what the project looks like in Eclipse:

Project in Eclipse

Above are the project contents. Next, execute the mvn clean package from the command prompt or terminal. Then, execute java -jar target\sample-0.0.1.jar.

You can also launch the application by running the SampleApplication.java class from your IDE.

Now, let's visit the Swagger UI — http://localhost:8080/swagger-ui.html:

Swagger UI

Expand the > symbol on the right of Person under Schemas. Expand the various properties also.

Expand the > symbol on the right of Person under Schemas and various properties

The nice thing is how the contract is automatically detailed leveraging JSR-303 annotations on the model. Out-of-the-box, it covers many of the important annotations and documents them. However, I do not see it support  @javax.validation.constraints.Email and @org.hibernate.validator.constraints.CreditCardNumber out-of-the-box at this point. 

Nor does it support our custom annotation which we applied to the registrationDate field in the Person Java bean.

Java
 
@LocalDateTimeFormat(pattern = "yyyyMMdd",   dateTimeType=DateTimeType.Date, 
                     message = "Invalid dateTimeField Format. It Should be in yyyyMMdd format")
private String registrationDate;


However, these constraints are being applied by the back end.

For completeness, let's post a request. Press the POST button. Then press the Try it out button that will appear. That will lead you to the screen below.

Posting a request

Press the blue Execute button.

Execute request

JSON
 
{
  "timestamp": "2024-04-07T12:49:49.106+00:00",
  "status": 400,
  "error": "Bad Request",
  "message": "Validation failed for object='person'. Error count: 5",
  "errors": [
    {
      "codes": [
        "CreditCardNumber.person.creditCardNumber",
        "CreditCardNumber.creditCardNumber",
        "CreditCardNumber.java.lang.String",
        "CreditCardNumber"
      ],
      "arguments": [
        {
          "codes": [
            "person.creditCardNumber",
            "creditCardNumber"
          ],
          "arguments": null,
          "defaultMessage": "creditCardNumber",
          "code": "creditCardNumber"
        },
        false
      ],
      "defaultMessage": "invalid credit card number",
      "objectName": "person",
      "field": "creditCardNumber",
      "rejectedValue": "411111111111111",
      "bindingFailure": false,
      "code": "CreditCardNumber"
    },
    {
      "codes": [
        "LocalDateTimeFormat.person.registrationDate",
        "LocalDateTimeFormat.registrationDate",
        "LocalDateTimeFormat.java.lang.String",
        "LocalDateTimeFormat"
      ],
      "arguments": [
        {
          "codes": [
            "person.registrationDate",
            "registrationDate"
          ],
          "arguments": null,
          "defaultMessage": "registrationDate",
          "code": "registrationDate"
        },
        "Date",
        {
          "arguments": null,
          "codes": [
            "yyyyMMdd"
          ],
          "defaultMessage": "yyyyMMdd"
        }
      ],
      "defaultMessage": "Invalid dateTimeField Format. It Should be in yyyyMMdd format",
      "objectName": "person",
      "field": "registrationDate",
      "rejectedValue": "string",
      "bindingFailure": false,
      "code": "LocalDateTimeFormat"
    },
    {
      "codes": [
        "Min.person.age",
        "Min.age",
        "Min.int",
        "Min"
      ],
      "arguments": [
        {
          "codes": [
            "person.age",
            "age"
          ],
          "arguments": null,
          "defaultMessage": "age",
          "code": "age"
        },
        18
      ],
      "defaultMessage": "must be greater than or equal to 18",
      "objectName": "person",
      "field": "age",
      "rejectedValue": 17,
      "bindingFailure": false,
      "code": "Min"
    },
    {
      "codes": [
        "Email.person.email1",
        "Email.email1",
        "Email.java.lang.String",
        "Email"
      ],
      "arguments": [
        {
          "codes": [
            "person.email1",
            "email1"
          ],
          "arguments": null,
          "defaultMessage": "email1",
          "code": "email1"
        },
        [],
        {
          "arguments": null,
          "codes": [
            ".*"
          ],
          "defaultMessage": ".*"
        }
      ],
      "defaultMessage": "must be a well-formed email address",
      "objectName": "person",
      "field": "email1",
      "rejectedValue": "abcabc.com",
      "bindingFailure": false,
      "code": "Email"
    },
    {
      "codes": [
        "Pattern.person.email",
        "Pattern.email",
        "Pattern.java.lang.String",
        "Pattern"
      ],
      "arguments": [
        {
          "codes": [
            "person.email",
            "email"
          ],
          "arguments": null,
          "defaultMessage": "email",
          "code": "email"
        },
        [],
        {
          "arguments": null,
          "codes": [
            ".+@.+\\..+"
          ],
          "defaultMessage": ".+@.+\\..+"
        }
      ],
      "defaultMessage": "Please provide a valid email address",
      "objectName": "person",
      "field": "email",
      "rejectedValue": "abcabc.com",
      "bindingFailure": false,
      "code": "Pattern"
    }
  ],
  "path": "/person"
}


It's obvious from the above JSON that these annotations did get applied by the back end:

  • @javax.validation.constraints.Email
  • @org.hibernate.validator.constraints.CreditCardNumber
  • @sample.customvalidationsLocalDateTimeFormat

Let's feed in a valid input: 

JSON
 
{
  "id": 0,
  "firstName": "string",
  "lastName": "string",
  "email": "abc@abc.com",
  "email1": "abc@abc.com",
  "age": 20,
  "creditCardNumber": "4111111111111111",
  "registrationDate": "20211231"
}


Let's feed that valid input into the Request body section. (We can also select "validRequest" from the Examples dropdown as shown below.)

Feed valid input into the request body section

On pressing the blue Execute button, we see the below:

Execution output

The code so far can be found here:

  • Git Clone URL:   branch: custom-validators-updated1

Let's revisit our objective:

New objective: Wouldn't it be nice if we could extend Swagger's behavior and communicate via its automated documentation the information regarding these additional annotations and also custom validation annotations?

Let's work on exactly this now.

Let's add two new classes.

Java
 
package sample.config;

import lombok.AllArgsConstructor;
import lombok.Data;

@Data
@AllArgsConstructor
class DateTimeFormatData {

	private String pattern;

	private String dateTimeType;

}


Java
 
package sample.config;

import java.lang.annotation.Annotation;
import java.util.Map;

import com.fasterxml.jackson.databind.ObjectMapper;
import io.swagger.v3.core.jackson.ModelResolver;
import io.swagger.v3.oas.models.media.Schema;
import sample.customvalidations.LocalDateTimeFormat;

import org.springframework.context.annotation.Configuration;

@Configuration
class CustomOpenApiValidator extends ModelResolver {

	private final Class[] handledValidations = { jakarta.validation.constraints.NotNull.class,
			jakarta.validation.constraints.NotBlank.class,
			jakarta.validation.constraints.NotEmpty.class,
			jakarta.validation.constraints.Min.class,
			jakarta.validation.constraints.Max.class,
			jakarta.validation.constraints.DecimalMin.class,
			jakarta.validation.constraints.DecimalMax.class,
			jakarta.validation.constraints.Pattern.class,
			jakarta.validation.constraints.Size.class };

	private final Package[] allowedPackages = { handledValidations[0].getPackage(),
			org.hibernate.validator.constraints.CreditCardNumber.class.getPackage(),
			LocalDateTimeFormat.class.getPackage() };

	public CustomOpenApiValidator(ObjectMapper mapper) {
		super(mapper);
	}

	@Override
	protected void applyBeanValidatorAnnotations(Schema property, Annotation[] annotations, Schema parent, boolean applyNotNullAnnotations) {
		super.applyBeanValidatorAnnotations(property, annotations, parent, applyNotNullAnnotations);
		if (annotations != null) {
			for (Annotation annotation : annotations) {
				Class<? extends Annotation> annotationType = annotation.annotationType();
				boolean handled = false;
				for (Class check : handledValidations) {
					if (annotationType == check) {
						handled = true;
						break;
					}
				}
				if (!handled) {
					Package annotationPackage = annotationType.getPackage();
					boolean allowed = false;
					for (Package allowedPackage : allowedPackages) {
						if (allowedPackage == annotationPackage) {
							allowed = true;
							break;
						}
					}
					if (allowed) {
						Map extensions = property.getExtensions();
						String extensionKey = "x-" + annotationType.getSimpleName();
						if (!(extensions != null && extensions.containsKey(extensionKey))) {
							Object value = describeAnnotation(annotation, annotationType);
							property.addExtension(extensionKey, value);

						}
					}
				}
			}
		}

	}

	private Object describeAnnotation(Annotation annotation, Class<? extends Annotation> annotationType) {
		Object ret = true;
		if (annotationType == LocalDateTimeFormat.class) {
			LocalDateTimeFormat format = (LocalDateTimeFormat) annotation;
			ret = new DateTimeFormatData(format.pattern(), format.dateTimeType().name());

		}
		return ret;
	}
}




Please stop the application, then build and restart the application. Revisit the Swagger UI at http://localhost:8080/swagger-ui.html. If you check, you will find that the schemas now show the highlighted extensions to convey additional constraints.

Schemas now show the highlighted extensions to convey additional constraints

Also as shown below:

Schemas output

Conclusion

We have shown how to meet our new objective, demonstrated how to use Swagger schema extensions, and documented the additional constraints that would have otherwise remained undocumented. This includes custom validators. In the next part, we will try something along these same lines but stretch the limits a little more.

  • Source code
  • Git Clone URL, branch: documented-custom-validators-updated1

Troubleshooting Tips

  • Ensure prerequisites.
  • If using the Eclipse IDE, we might need to do a Maven update on the project (right-click on Project  - Maven > Update Project) after creating all the files.
  • In the Swagger UI, if you are unable to access the “Schema” definitions link, it might be because you need to come out of the “try it out “ mode. Click on one or two Cancel buttons that could be visible.
  • Ensure you use http://localhost:8080/swagger-ui.html for this tutorial.
  • Also, see Using Lombok for setting up Lombok in the IDE.
Spring Framework API Java (programming language) Spring Boot Documentation Apache Maven

Opinions expressed by DZone contributors are their own.

Related

  • Exploring Hazelcast With Spring Boot
  • How To Build Web Service Using Spring Boot 2.x
  • OpenAPI 3 Documentation With Spring Boot
  • Spring Microservices RESTFul API Documentation With Swagger Part 1

Partner Resources

×

Comments
Oops! Something Went Wrong

The likes didn't load as expected. Please refresh the page and try again.

ABOUT US

  • About DZone
  • Support and feedback
  • Community research
  • Sitemap

ADVERTISE

  • Advertise with DZone

CONTRIBUTE ON DZONE

  • Article Submission Guidelines
  • Become a Contributor
  • Core Program
  • Visit the Writers' Zone

LEGAL

  • Terms of Service
  • Privacy Policy

CONTACT US

  • 3343 Perimeter Hill Drive
  • Suite 100
  • Nashville, TN 37211
  • support@dzone.com

Let's be friends:

Likes
There are no likes...yet! 👀
Be the first to like this post!
It looks like you're not logged in.
Sign in to see who liked this post!