Generating PDF documentation from the OpenAPI specification in a SpringBoot application

Preface

This article may be useful to those who are looking for ways to automatically generate PDF documentation to describe the API of a developed SpringBoot application.

Description of the problem

When integrating with our application, written on the “classic” SpringBoot stack, the question arose about providing an API description to the partner. Almost out of the box, SpringBoot allows you to deploy a thin Swagger client on the side of your application and generate on the fly a specification in the Swagger format (OpenAPI), which is JSON of a special structure (although if the reader does not know what it is, there is probably no point in reading this article at all ).

The problem was complicated by the fact that our partner developed in 1C, and all modern specifications were too difficult for him to get used to, so the task arose to provide documentation in a human-oriented form – DOC, PDF, etc. I’m afraid to imagine how shocked he would be by the specification in WSDL format (but I’m already deviating from the topic).

During research on Google, an article was found – https://www.baeldung.com/swagger-generate-pdf, and tips on stackoverflow that actually repeated this article. In fact, only 2 solutions were found:

1) Use an online converter https://www.swdoc.org/

2) Set up a chain of 3 maven plugins:

  • swagger-maven-plugin generates the swagger specification

  • swagger2markup-maven-plugin based on the swagger specification generates documentation in ASCII-doctor format

  • And already asciidoctor-maven-plugin generates PDF documentation based on ASCII documentation

To minimize labor costs, the first option was chosen – simply generate PDF documentation in an online converter. And for some time it seemed that the problem was solved.

However, it soon became clear that the online converter only works correctly with specifications in the format Swagger V2which is generated by our legacy services that use the swagger library to support springfox. Most new services use the library for these purposes springdocwhich already generates a specification in Swagger V3 format (aka OpenAPI 3). When trying to generate a PDF from the specification of this format, the checkbox of the same name on the online converter website saved little – the PDF file was either not generated at all and an error popped up, or it was converted crookedly and “lost” a lot of data from the specification, for example, information about types in the POST method or description of individual fields. An assumption was made that the site was not working entirely correctly, and it was decided configure the necessary maven plugins dive into a wormhole. At first glance it seemed that it was literally a matter of 20 minutes – came in and immediately left copy the plugin config and run the command “mvn package”

It was necessary to add some kind of picture so that it would not be so boring to read a sheet of text

It was necessary to add some kind of picture so that it would not be so boring to read a sheet of text

In a wormhole

So, even during the initial search for a solution, advice was found that swagger-maven-plugin for generating swagger annotations, they are not really needed, although this is indicated almost everywhere. It is enough to write a test in which the Spring context is raised and exactly the specification generated by the library is downloaded springdoc, and this file is saved to the build directory for further processing. This eliminates the chance that swagger-maven-plugin handles swagger annotations differently than springdoc.

@Slf4j
@SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT)
public class PersistOpenApiTest {

	private static final String PERSIST_PATH = "target/api-doc.json";

	@LocalServerPort
	int localPort;

	@Value("${springdoc.api-docs.path}")
	String  apiDocPath;

	@Autowired
	ObjectMapper mapper;

	RestTemplate restTemplate = new RestTemplate();

	@Test
	public void persistOpenApiSpec() throws Exception {
		log.info("loading openApiSpec");
		String openApiSpec = restTemplate.getForObject("http://localhost:" + localPort +  apiDocPath, String.class);
		log.info("openApiSpec is {}", openApiSpec);
		Assertions.assertNotNull(openApiSpec);
		Files.writeString(Paths.get(PERSIST_PATH), prettyJson(openApiSpec));
	}

	private String prettyJson(String json) throws Exception {
		var mapSpec = mapper.readValue(json, Map.class);
		return mapper.writer().withDefaultPrettyPrinter()
				.writeValueAsString(mapSpec);
	}
}

Writing such a test is not difficult.

The next step is to configure the plugin swagger2markup-maven-plugin, which, based on the swagger specification, generates documentation in ASCII-doctor format. And this is where the fun begins.

Turns out, swagger2markup-maven-plugin requires 2 dependencies that are not in the maven-central repository. More precisely, there is data about them, but it is indicated that they are in the Jcenter repository, which ceased to exist in 2021 (if anyone didn’t know), and in fact they are not preserved anywhere… In the github project swagger2markup an issue was found about this, but there was no feedback from the developers.

But we were lucky – we managed to find one jar file on some Chinese site, and even with the help of a translator we found which hieroglyph means “download” and saved this library. With the second library, the situation is a little more complicated, I had to download the sources, learn a little about working with gradle, cut out all the links to jcenter from the project, and assemble the library from the sources.

The whole thing took 3-4 hours, the daily started, and the PM said that I was working on hu… nonsense, it’s easier to get our 1C specialist partners to work directly with the OpenAPI specification in JSON format.

Thus, we can conclude that on everyone’s favorite site baeldung.com, on stackoveflow and in other sources there is simply no working solution to the problem (or I could not find it).

Just kidding, it would be stupid if the story ended there. This problem seemed very interesting and non-standard to me, and I felt that I was already almost one step away from solving it. So I had to continue solving it in my free time from work, on a day off Your humble servant typical redneck coder.

There was just a little bit left to do – configure the latest asciidoctor-maven-plugin plugin and start the build. Since we are fashionable and stylish, we took not the dense version from the article on baeldung, but the latest available version in the maven repository. I was no longer surprised that nothing came together and the plugin crashed with an error. I did a little searching and found a working example in the official asciidoctor-maven-plugin repository on github. It turned out that by default the plugin pulls up the ruby ​​version with which it itself conflicts, but surprisingly in the repository with examples they took this into account and redefined it to the working version of ruby. Why the plugin itself uses a non-working version of ruby ​​remains a mystery, shrouded in darkness. The more you dive into the world of OpenSource technologies, the more questions arise…

As a result, everything came together, started, a PDF file was generated based on the OpenAPI 3 specification, and we came to where we started – it was the same “curve” as when generated in the online converter.

It turned out that the plugin swagger2markup-maven-plugin not only uses currently inaccessible dependencies, it is simply dead – the last release was in 2018, and judging by the issue on github, work was underway to support OpenAPI 3 until about 2020, but they were not completed, and the plugin only supports Swagger 2. Well, in 2020 its maintainer Robert Winkler published an open appeal to the community that he could not cope alone and was looking for someone to pass this burden on, and apparently, he did not find anyone.

Dead end. Dejection and hopelessness

So I’m back where I started. What were the options?

  • Fork from swagger2markup-maven-plugin, and complete support for OpenApi3, apparently some groundwork has already been implemented, but was not completed. The only question is, how many hours of labor does this option require?

  • Previously, during a search on Google, projects on github based on JS were found that already implement this functionality; perhaps it was worth trying to implement a maven plugin that uses the launch of these solutions?

  • Continue google-research, go deeper into page 3-4 of the search results and hope that someone has solved this problem…

Obviously, you need to start from the end, since the first option is the most labor-intensive, the second less expensive, and the third with minimal costs. And I’m lucky!

Working solution

It turns out that there are documentation generators in the openapi-generator project https://openapi-generator.tech/docs/generators/#documentation-generators

I had previously used openapi-generator-maven-plugin, but I assumed that it was capable of generating only a client and a class model for accessing the service based on the specification. However, it turned out that by specifying the required generator name, instead of a client, it is able to generate not a client, but documentation – in html, html2 and asciidoctor formats. Interestingly, this was only a hypothesis, it was not written about this directly anywhere, and it had to be tested to make sure the solution worked.

<plugin>
	<!--
		OpenApi provides generators for generation documentation
		https://openapi-generator.tech/docs/generators#documentation-generators
	-->
	<groupId>org.openapitools</groupId>
	<artifactId>openapi-generator-maven-plugin</artifactId>
	<version>7.1.0</version>
	<executions>
		<execution>
			<id>open-api-doc-html</id>
			<phase>prepare-package</phase>
			<goals>
				<goal>generate</goal>
			</goals>
			<configuration>
				<inputSpec>${project.basedir}/target/api-doc.json</inputSpec>
				<output>${project.basedir}/target/generated-doc/html</output>
				<!-- https://openapi-generator.tech/docs/generators/html/ -->
				<generatorName>html</generatorName>
			</configuration>
		</execution>
		<execution>
			<id>open-api-doc-html2</id>
			<phase>prepare-package</phase>
			<goals>
				<goal>generate</goal>
			</goals>
			<configuration>
				<inputSpec>${project.basedir}/target/api-doc.json</inputSpec>
				<output>${project.basedir}/target/generated-doc/html2</output>
				<!-- https://openapi-generator.tech/docs/generators/html2/ -->
				<generatorName>html2</generatorName>
			</configuration>
		</execution>
		<execution>
			<id>open-api-doc-asciidoc</id>
			<phase>prepare-package</phase>
			<goals>
				<goal>generate</goal>
			</goals>
			<configuration>
				<inputSpec>${project.basedir}/target/api-doc.json</inputSpec>
				<output>${project.basedir}/target/generated-doc/asciidoc</output>
				<!-- https://openapi-generator.tech/docs/generators/asciidoc/ -->
				<generatorName>asciidoc</generatorName>
			</configuration>
		</execution>
	</executions>
</plugin>

The plugin, configured in the above way, generates documentation in 3 formats – html, html2 and asciidoctor. Html and html2 are already 2 formats that are convenient for human perception and can be sent directly to your partners.

However, if you still need PDF, then you need to configure another plugin that will generate a PDF file based on asciidoctor.

<properties>
	<!--
		Default asciidoctor dependencies conflicted, use solution from
		https://github.com/asciidoctor/asciidoctor-maven-examples/blob/main/asciidoctor-pdf-example/pom.xml
	-->
	<asciidoctor.maven.plugin.version>2.2.4</asciidoctor.maven.plugin.version>
	<asciidoctorj.pdf.version>2.3.9</asciidoctorj.pdf.version>
	<asciidoctorj.version>2.5.10</asciidoctorj.version>
	<jruby.version>9.4.2.0</jruby.version>
</properties>

<!-- .... -->

<plugin>
	<groupId>org.asciidoctor</groupId>
	<artifactId>asciidoctor-maven-plugin</artifactId>
	<version>${asciidoctor.maven.plugin.version}</version>
	<dependencies>
		<dependency>
			<groupId>org.asciidoctor</groupId>
			<artifactId>asciidoctorj-pdf</artifactId>
			<version>${asciidoctorj.pdf.version}</version>
		</dependency>
		<!-- Comment this section to use the default jruby artifact provided by the plugin -->
		<dependency>
			<groupId>org.jruby</groupId>
			<artifactId>jruby</artifactId>
			<version>${jruby.version}</version>
		</dependency>
		<!-- Comment this section to use the default AsciidoctorJ artifact provided by the plugin -->
		<dependency>
			<groupId>org.asciidoctor</groupId>
			<artifactId>asciidoctorj</artifactId>
			<version>${asciidoctorj.version}</version>
		</dependency>
	</dependencies>
	<configuration>
		<sourceDirectory>${project.basedir}/target/generated-doc/asciidoc</sourceDirectory>
		<outputDirectory>${project.basedir}/target/generated-doc/pdf</outputDirectory>
	</configuration>
	<executions>
		<execution>
			<id>generate-pdf-doc</id>
			<phase>prepare-package</phase>
			<goals>
				<goal>process-asciidoc</goal>
			</goals>
			<configuration>
				<backend>pdf</backend>
				<attributes>
					<source-highlighter>rouge</source-highlighter>
					<icons>font</icons>
					<pagenums/>
					<toc/>
					<idprefix/>
					<idseparator>-</idseparator>
				</attributes>
			</configuration>
		</execution>
	</executions>
</plugin>

As a result, we have 5 documentation options

  • the specification itself in OpenAPI 3 format (saved in the unit test)

  • html

  • html2 (“pretty” html)

  • asciidoc

  • pdf

If you wish, you can study the settings of each generator (links are provided in the comments in the plugin configuration).

The source code of the demo service and configured plugins are posted on github https://github.com/shmakovalexey/sw-pdf-example/blob/main/service/pom.xml

PS: the reader may have a question – why couldn’t I immediately describe a working solution, but described all my ordeals? My answer is simple – I spent more than one hour of working time, and much more personal time, to come to a working solution, so I think it would be fair if the reader felt at least a small echo of this pain. And for the English-speaking community, I described the method I found in the most popular question on Stackoverflow on this topic.

Similar Posts

Leave a Reply

Your email address will not be published. Required fields are marked *