API documentation (Swagger)

indexmenu_n_60

We use swagger specification for RESTful API documentation with tools:

  • Swagger UI - visualize and interact with the API’s resources. It is available at IdM backend url <server>/idm-backend/api.
  • Swagger2Markup - generation of an up-to-date RESTful API documentation by combining documentation that’s been hand-written with auto-generated API documentation produced by Swagger. The result is intended to be an up-to-date, easy-to-read, on- and offline user guide.
  • Asciidoctor maven plugin - official way to convert AsciiDoc documentation using Asciidoctor from an Maven build.
Difference between dynamic and static documentation:
  • Dynamic documentation is exposed by Swagger UI from rest controllers with @Api annotation. Documentation is available on root backend server url e.g.<server>/api - documented endpoints can be tested with demo credentials directly from Swagger UI. Documentation is splitted by modules. Raw Swagger specification in json format is available on url e.g. <server>/api/doc?group=core. Documentation always contains authentication endpoint (security information).
  • Static documentation is generated from raw swagger specification (e.g. <server>/api/doc?group=core) and static content (see next chapter with static documentation folder structure). Documentation is splitted by modules. Generated output html can be found in <module>/target/asciidoc/html/index.html. This documentation can be exposed as api reference and is exposed directly in application, which is built under release profile on urls with convention <server>/webjars/<module>/<version>/doc/index.html.

Parent project contains basic settings for module documentation. When a new module is added, some steps have to be done. Complete configuration can be found in example module.

Module properties

Use PropertyModuleDescriptor generalization for module descriptor definition and prepare module-example.properties.

/**
 * Example module descriptor
 */
@Component
@PropertySource("classpath:module-" + ExampleModuleDescriptor.MODULE_ID + ".properties")
@ConfigurationProperties(prefix = "module." + ExampleModuleDescriptor.MODULE_ID + ".build", ignoreUnknownFields = true, ignoreInvalidFields = true)
public class ExampleModuleDescriptor extends PropertyModuleDescriptor {
 
	public static final String MODULE_ID = "example";
 
	@Override
	public String getId() {
		return MODULE_ID;
	}
 
        /**
	 * Enables links to swagger documentation
	 */
	@Override
	public boolean isDocumentationAvailable() {
		return true;
	}
}

Swagger endpoint

/**
 * Example module swagger configuration
 */
@Configuration
@ConditionalOnProperty(prefix = "springfox.documentation.swagger", name = "enabled", matchIfMissing = true)
public class ExampleSwaggerConfig extends AbstractSwaggerConfig {
 
	@Autowired private ExampleModuleDescriptor moduleDescriptor;
 
	@Override
	protected ModuleDescriptor getModuleDescriptor() {
		return moduleDescriptor;
	}
 
	@Bean
	public Docket exampleApi() {
		return api("eu.bcvsolutions.idm.example");
	}
}

springdoc version (13.1.0+)

/**
 * Example module swagger configuration
 */
@Configuration
@ConditionalOnProperty(prefix = "springdoc.swagger-ui", name = "enabled", matchIfMissing = true)
public class ExampleSwaggerConfig extends AbstractSwaggerConfig {
 
	@Autowired private ExampleModuleDescriptor moduleDescriptor;
 
	@Override
	protected ModuleDescriptor getModuleDescriptor() {
		return moduleDescriptor;
	}
 
	@Bean
	public GroupedOpenApi exampleApi() {
		return api("eu.bcvsolutions.idm.example.rest");
	}
}

Rest controller

Add swagger annotations to controllers.

/**
 * Ping pong example controller
 */
@RestController
@RequestMapping(value = BaseController.BASE_PATH + "/examples", produces = BaseController.APPLICATION_HAL_JSON_VALUE)
@Api(value = "Examples", description = "Example operations", tags = { "Examples" })
public class ExampleController {
 
	@ResponseBody
	@RequestMapping(method = RequestMethod.GET)
	@ApiOperation(
			value = "Ping - Pong operation", 
			notes= "Returns message with additional informations",
			nickname = "ping", 
			tags={ "Examples" }, 
			response = Pong.class, 
			authorizations = {
				@Authorization(SwaggerConfig.AUTHENTICATION_BASIC),
				@Authorization(SwaggerConfig.AUTHENTICATION_CIDMST)
			})
	public ResponseEntity<?> ping(
			@ApiParam(value = "In / out message", example = "hello", defaultValue = "hello") 
			@RequestParam(required = false, defaultValue = "hello") String message
			) {
		return new ResponseEntity<>(new Pong(message), HttpStatus.OK); 
	}
}

For springdoc version (13.1.0+) add new swagger annotations to controllers.

/**
 * Ping pong example controller
 */
@RestController
@Enabled(ExampleModuleDescriptor.MODULE_ID)
@RequestMapping(value = BaseController.BASE_PATH + "/examples")
@Tag(name = ExampleController.TAG, description = "Example operations")
 
public class ExampleController {
 
	protected static final String TAG = "Examples";
	@Autowired private ExampleService service;
 
	@ResponseBody
	@RequestMapping(method = RequestMethod.GET, path = "/ping")
	@Operation(
		summary = "Ping - Pong operation", 
		description= "Returns message with additional informations",
		operationId = "ping",
                tags={ ExampleController.TAG }
                responses = @ApiResponse(
                    responseCode = "200",
                    content = {
                            @Content(
                                    mediaType = BaseController.APPLICATION_HAL_JSON_VALUE,
                                    schema = @Schema(
                                            implementation = Pong.class
                                    )
                            )
                    }
            ))
    @SecurityRequirements({
        @SecurityRequirement(name = SwaggerConfig.AUTHENTICATION_BASIC),
        @SecurityRequirement(name = SwaggerConfig.AUTHENTICATION_CIDMST)
    })
	public ResponseEntity<Pong> ping(
			@Parameter(description = "In / out message", example = "hello")
			@RequestParam(required = false, defaultValue = "hello") String message
			) {
		return new ResponseEntity<>(service.ping(message), HttpStatus.OK); 
	}
}

Model

Add swagger annotations to dtos.

/**
 * Example ping - pong response dto
 */
@ApiModel(description = "Ping - Pong response")
public class Pong implements BaseDto {
 
	private static final long serialVersionUID = 1L;
	//
	@ApiModelProperty(required = true, notes = "Unique pong identifier")
	private UUID id;
	@ApiModelProperty(notes = "Ping - Pong response message")
	private String message;
	@ApiModelProperty(required = true, notes = "Creation time")
	private DateTime created;
 
	// ... getters, setters
}

For springdoc version (13.1.0+) add new swagger annotations to dtos.

/**
 * Example ping - pong response dto
 */
@Schema(description = "Ping - Pong response")
public class Pong implements BaseDto {
 
	private static final long serialVersionUID = 1L;
	//
	@Schema(requiredMode = Schema.RequiredMode.REQUIRED, description = "Unique pong identifier")
	private UUID id;
	@Schema(description = "Ping - Pong response message")
	private String message;
	@Schema(requiredMode = Schema.RequiredMode.REQUIRED, description = "Creation time")
	private ZonedDateTime created;
 
	// ... getters, setters
}

Test

Create integration test:

/**
 * Static swagger generation to sources - will be used as input for swagger2Markup build
 */
public class Swagger2MarkupTest extends AbstractSwaggerTest {
 
	@Test
	public void testConvertSwagger() throws Exception {
		super.convertSwagger(ExampleModuleDescriptor.MODULE_ID);
	}
 
}

We are using asciidoctor maven plugin for static documentation (see maven chapter). Maven plugins prepare all generated artifacts, but requires some static artifacts with static / written documentation in structure in module sources folder:

  • <module>/src/docs/asciidoc/ - root documentation folder
    • index.adoc - main documentation file / entrypoint. Contains documentation structure, includes all other generated and extension files.
    • extensions - contains files with extensions
      • definitions - api models
      • overview - antre section
      • paths - controller paths
      • security - security section

All files have to be written in asciidoc format. Read more about extensions in swagger2markup documentation.

Example extension

Add security information about demo identity credentials. Create file <module>/src/docs/asciidoc/extensions/security/document-begin-text.adoc with content:

== Demo credentials
 
admin / admin

This content will be automatically included to static documentation to the security section. Read more about available extension points.

Documentation is generated under release profile. Add profile to module pom.xml:

<profile>
	<id>release</id>
	<build>
		<plugins>
			<!-- First, use the swagger2markup plugin to generate asciidoc -->
			<plugin>
				<groupId>io.github.swagger2markup</groupId>
				<artifactId>swagger2markup-maven-plugin</artifactId>
				<version>${swagger2markup.version}</version>
 
				<configuration>
					<swaggerInput>${swagger.input}</swaggerInput>
					<outputDir>${generated.asciidoc.directory}</outputDir>
					<config>
						<swagger2markup.markupLanguage>ASCIIDOC</swagger2markup.markupLanguage>
						<swagger2markup.outputLanguage>EN</swagger2markup.outputLanguage>
						<swagger2markup.pathsGroupedBy>TAGS</swagger2markup.pathsGroupedBy>
						<swagger2markup.generatedExamplesEnabled>false</swagger2markup.generatedExamplesEnabled>
 
						<swagger2markup.extensions.dynamicOverview.contentPath>${asciidoctor.input.extensions.directory}/overview</swagger2markup.extensions.dynamicOverview.contentPath>
						<swagger2markup.extensions.dynamicDefinitions.contentPath>${asciidoctor.input.extensions.directory}/definitions</swagger2markup.extensions.dynamicDefinitions.contentPath>
						<swagger2markup.extensions.dynamicPaths.contentPath>${asciidoctor.input.extensions.directory}/paths</swagger2markup.extensions.dynamicPaths.contentPath>
						<swagger2markup.extensions.dynamicSecurity.contentPath>${asciidoctor.input.extensions.directory}/security/</swagger2markup.extensions.dynamicSecurity.contentPath>
 
						<swagger2markup.extensions.springRestDocs.snippetBaseUri>${swagger.snippetOutput.dir}</swagger2markup.extensions.springRestDocs.snippetBaseUri>
						<swagger2markup.extensions.springRestDocs.defaultSnippets>true</swagger2markup.extensions.springRestDocs.defaultSnippets>
					</config>
				</configuration>
				<executions>
					<execution>
						<phase>test</phase>
						<goals>
							<goal>convertSwagger2markup</goal>
						</goals>
					</execution>
				</executions>
			</plugin>
 
			<!--
				Run the generated asciidoc through Asciidoctor to generate other
				documentation types, such as PDFs or HTML5
			-->
			<plugin>
				<groupId>org.asciidoctor</groupId>
				<artifactId>asciidoctor-maven-plugin</artifactId>
				<version>1.5.3</version>
				<!-- Configure generic document generation settings -->
				<configuration>
					<sourceDirectory>${asciidoctor.input.directory}</sourceDirectory>
					<sourceDocumentName>index.adoc</sourceDocumentName>
					<attributes>
						<doctype>book</doctype>
						<toc>left</toc>
						<toclevels>2</toclevels> <!-- Resources by tag names in menu only -->
						<numbered />
						<hardbreaks />
						<sectlinks />
						<sectanchors />
						<generated>${generated.asciidoc.directory}</generated>
					</attributes>
				</configuration>
				<!--
					Since each execution can only handle one backend, run separate
					executions for each desired output type
				-->
				<executions>
					<execution>
						<id>output-html</id>
						<phase>test</phase>
						<goals>
							<goal>process-asciidoc</goal>
						</goals>
						<configuration>
							<backend>html5</backend>
							<!-- static documentation will be available as webjars -->
							<!-- e.g. http://localhost:8080/idm/webjars/core/7.3.0/doc/index.html -->
							<outputDirectory>${asciidoctor.html.output.directory.prefix}/core/${project.version}/doc</outputDirectory>
						</configuration>
					</execution>
				</executions>
			</plugin>
		</plugins>
	</build>
</profile>

All maven properties are preconfigured in parent pom.xml:

<swagger.version>2.7.0</swagger.version>
<swagger2markup.version>1.3.1</swagger2markup.version>
 
<!-- 
	properties are used ind test and doc profile,
	test: swagger.json input is generated
	doc: swagger2markup and asciidoctor plugins require them
 -->
<asciidoctor.input.directory>${project.basedir}/src/docs/asciidoc</asciidoctor.input.directory>
<asciidoctor.input.extensions.directory>${asciidoctor.input.directory}/extensions</asciidoctor.input.extensions.directory>
<swagger.output.dir>${project.build.directory}/swagger</swagger.output.dir>
<swagger.output.filename>swagger.json</swagger.output.filename>
<swagger.input>${swagger.output.dir}/${swagger.output.filename}</swagger.input>
<swagger.snippetOutput.dir>${project.build.directory}/asciidoc/snippets</swagger.snippetOutput.dir>
<generated.asciidoc.directory>${project.build.directory}/asciidoc/generated</generated.asciidoc.directory>
<!-- static documentation will be available as webjars -->
<!-- e.g. http://localhost:8080/idm/webjars/core/7.3.0/doc/index.html -->
<asciidoctor.html.output.directory.prefix>${project.build.directory}/classes/META-INF/resources/webjars</asciidoctor.html.output.directory.prefix>
<asciidoctor.html.output.directory>${asciidoctor.html.output.directory.prefix}/${project.artifactId}/${project.version}/doc</asciidoctor.html.output.director
  • Add Swagger annotation. What can be written into annotation, will be written to annotation - will be shown in dynamic and static documentation. Static documentation extension is used, when annotation doesn't fit.
  • Use module-<module>.properties with PropertyModuleDescriptor.
  • Use produces = BaseController.APPLICATION\_HAL\_JSON\_VALUE in controller mapping
  • Use @ApiOperation(nickname = "<operatonId>") e.g. @ApiOperation(nickname = "ping") for controller methods - nickname (⇒ operationId) can be used in permalink.

in springdoc version (13.1.0+)

  • Add Swagger annotation. What can be written into annotation, will be written to annotation - will be shown in dynamic and static documentation. Static documentation extension is used, when annotation doesn't fit.
  • Use module-<module>.properties with PropertyModuleDescriptor.
  • produces = BaseController.APPLICATION\_HAL\_JSON\_VALUE is set by default but you can override it
  • Use @Operation(operationId= "<operatonId>") e.g. @Operation(operationId= "ping") for controller methods can be used in permalink.

When module aggregator is built under release profile, then static html and javadoc are packed into archive /target/<version>-doc.zip (tar.gz, tar.bz2).

cd aggregator/
mvn package -Prelease -DdocumentationOnly=true
Which documentation are packed into documentation archive is configured in /src/assembly/doc.xml descriptor. When new CzechIdM product module is created, don't forget to add new module here.

Use IdmIdentityController as inspiration.

Export swagger.json by running single test:

mvn clean package -DskipTests
mvn surefire:test -Dtest=Swagger2MarkupTest -Prelease

Generate static documentation only (swagger.json has to be exported - see previous tip). Generated html will be available in project's folder /target/classes/META-INF/resources/webjars/<module>/<version>/doc/index.html:

mvn package -DskipTests -Prelease

Static html documentation will be available from url (application's war has to be build under release profile):

<server>/webjars/<module>/<version>/doc/index.html
// e.g.
http://localhost:8080/idm/webjars/core/7.3.0-rc.4-SNAPSHOT/doc/index.html
  • Static documentation contains all files for now (copy / paste redundancy - see security section - same in all modules) - will be improved soon.
  • JSR303 for model documentation (comming soon)
  • Model SPI (comming soon)
  • UUID, GuardedString types (comming soon)
  • by chalupat