MicroProfile OpenAPI—Design First
This article explains how to generate code from a given OpenAPI document (Design First approach), using two different code generators (openapi-generator and swagger-codegen).
For an introduction to MicroProfile OpenAPI, the mini service used in this article and for the discussion of the Code First approach set the first article.
Design First
Actors of the Design First approach:
Legend
Quarkus is the framework powering the microservice running on the server
SmallRye is the MicroProfile implementation used by Quarkus
denotes the OpenAPI document format served at
/openapi
MicroProfile specifies the
/openapi
endpoint serving the OpenAPI document
Swagger Codegen is one of two code generators
OpenAPI Generator is the other code generator
The accompanying source code has two example projects for the Design First approach:
Both code generators target multiple programming languages (templates) and for some templates also flavours (sub-templates).
- Swagger Codegen
(GitHub, first release 2014),
an Apache-licensed open source project from SmartBear
(Company behind the brand Swagger)
- the best list of templates I could find is the source on GitHub itself: the list of language directories
- for the Java language exist about 9
library
sub-templates, again a list of directories
- OpenAPI Generator
(GitHub, first release 2018)
is a fork of Swagger Codegen and still
similar to use (see the configuration section below)
- the templates are called generators
- the sub-templates are called libraries, e.g. for the java generator
there are currently about 14
library
sub-templates (scroll to thelibrary
config option)
From openapi.yaml to code
Though both generators are able to generate whole projects (including server or client code), the scope of this article is the generation of request and response body POJOs only (the “models” or “schemas” of the API).
To start I show the OpenAPI schema part for PinCheckRequest (for easy comparison the generated OpenAPI parts from the first part of the article are shown on the second and third tab).
- design_first
- code_first_swagger
- code_first_quarkus
1 |
|
To keep the examples of the generated code short I show only one of the three properties that are defined in the OpenAPI document, this is enough to show the differences.
Swagger Codegen
The first generator is Swagger Codegen version 3.x (for the OpenAPI 3.x format), the still maintained 2.x line is intended for Swagger/OpenAPI 2.x and not in scope here.
Here’s part of the generated file PinCheckRequest.java
(generated with options language=java
and library=resteasy
; using the mustache template
pojo):
1/**
2 * Request for checking a PIN
3 */
4@Schema(description = "Request for checking a PIN")
5public class PinCheckRequest {
6 @JsonProperty("pan")
7 private ch.schlau.pesche.apidocs.swagger.designfirst.txproc.model.Pan pan = null;
8
9 @JsonProperty("pinBlock")
10 private String pinBlock = null;
11
12 @JsonProperty("uuid")
13 private UUID uuid = null;
14
15 public PinCheckRequest pan(ch.schlau.pesche.apidocs.swagger.designfirst.txproc.model.Pan pan) {
16 this.pan = pan;
17 return this;
18 }
19
20 /**
21 * Get pan
22 * @return pan
23 **/
24 @NotNull
25 @Valid
26 @Schema(required = true, description = "")
27 public ch.schlau.pesche.apidocs.swagger.designfirst.txproc.model.Pan getPan() {
28 return pan;
29 }
30
31 public void setPan(ch.schlau.pesche.apidocs.swagger.designfirst.txproc.model.Pan pan) {
32 this.pan = pan;
33 }
34}
- Swagger Codegen sprinkles the generated code with OpenAPI annotations,
e.g.
io.swagger.v3.oas.annotations.media.Schema
(line 4). I couldn’t find any option to disable this feature, meaning that your project has a possibly unwanted dependency toio.swagger.core.v3:swagger-annotations
- the generated POJO not only contains getters and setters, but also a “chaining setter”
(the
PinCheckRequest pan(Pan pan)
method on lines 15-18) - the code generator was configured to use a provided type
Pan
instead of generating a POJO (similar to thePan
type from the first article that was usingtype=SchemaType.STRING
as specialty). On lines 7, 15 etc. you can see the fully qualified class name for it.
The complete generated file including methods for the fields pinBlock
, uuid
and
the methods equals()
, hashCode()
, toString()
and toIndentedString
can be found in the GitHub repo.
OpenAPI Generator
Here’s part of the generated file PinCheckRequest.java
(generated with options generatorName=java
and library=native
; using the mustache template
pojo):
1/**
2 * Request for checking a PIN
3 */
4@JsonPropertyOrder({
5 PinCheckRequest.JSON_PROPERTY_PAN,
6 PinCheckRequest.JSON_PROPERTY_PIN_BLOCK,
7 PinCheckRequest.JSON_PROPERTY_UUID
8})
9@jakarta.annotation.Generated(value = "org.openapitools.codegen.languages.JavaClientCodegen", comments = "Generator version: 7.4.0")
10public class PinCheckRequest {
11 public static final String JSON_PROPERTY_PAN = "pan";
12 private ch.schlau.pesche.apidocs.openapi.designfirst.txproc.model.Pan pan;
13
14 public static final String JSON_PROPERTY_PIN_BLOCK = "pinBlock";
15 private String pinBlock;
16
17 public static final String JSON_PROPERTY_UUID = "uuid";
18 private UUID uuid;
19
20 public PinCheckRequest() {
21 }
22
23 public PinCheckRequest pan(ch.schlau.pesche.apidocs.openapi.designfirst.txproc.model.Pan pan) {
24 this.pan = pan;
25 return this;
26 }
27
28 /**
29 * Get pan
30 * @return pan
31 **/
32 @jakarta.annotation.Nonnull
33 @NotNull
34 @Valid
35
36 @JsonProperty(JSON_PROPERTY_PAN)
37 @JsonInclude(value = JsonInclude.Include.ALWAYS)
38
39 public ch.schlau.pesche.apidocs.openapi.designfirst.txproc.model.Pan getPan() {
40 return pan;
41 }
42
43 @JsonProperty(JSON_PROPERTY_PAN)
44 @JsonInclude(value = JsonInclude.Include.ALWAYS)
45 public void setPan(ch.schlau.pesche.apidocs.openapi.designfirst.txproc.model.Pan pan) {
46 this.pan = pan;
47 }
48}
- OpenAPI Generator uses (see lines 37 and 44)
JsonInclude.Include.ALWAYS
for required fields (andJsonInclude.Include.USE_DEFAULTS
for optional ones, see jackson_annotations.mustache) - the generated POJO not only contains getters and setters, but also a “chaining setter”
(the
PinCheckRequest pan(Pan pan)
method on lines 23-26) - the code generator was configured to use a provided type
Pan
instead of generating a POJO (similar to thePan
type from the first article that was usingtype=SchemaType.STRING
as specialty). On lines 12, 23 etc. you can see the fully qualified class name for it.
The complete generated file including methods for the fields pinBlock
, uuid
and
the methods equals()
, hashCode()
, toString()
and toIndentedString
can be found in the GitHub repo.
Toolchain Configuration
To have changes in the OpenAPI document automatically integrated into your code, you’d want to integrate the code generator into your workflow. As the example project is using Maven, here are important parts of the configuration section for both code generator’s Maven plugins.
Swagger Codegen’s io.swagger.codegen.v3:swagger-codegen-maven-plugin
configuration
Extract from the complete pom.xml
file:
1<configuration>
2 <inputSpec>${project.basedir}/src/main/resources/META-INF/openapi.yaml</inputSpec>
3 <language>java</language>
4 <configOptions>
5 <java8>true</java8>
6 <dateLibrary>java8</dateLibrary>
7 <library>resteasy</library>
8 <jakarta>true</jakarta>
9 <useBeanValidation>true</useBeanValidation>
10 <sourceFolder>.</sourceFolder>
11 <modelPackage>ch.schlau.pesche.apidocs.swagger.designfirst.generated.model</modelPackage>
12 </configOptions>
13 <importMappings>
14 <importMapping>Pan=ch.schlau.pesche.apidocs.swagger.designfirst.txproc.model.Pan</importMapping>
15 </importMappings>
16</configuration>
- lines 3 and 7: with Swagger Codegen the options to choose the programming language and flavour are
language
andlibrary
- line 14: the option to map a schema from openapi.yaml to an existing Java class is
importMapping
OpenAPI Generator’s org.openapitools:openapi-generator-maven-plugin
configuration
Extract from the complete pom.xml
file:
1<configuration>
2 <inputSpec>${project.basedir}/src/main/resources/META-INF/openapi.yaml</inputSpec>
3 <generatorName>java</generatorName>
4 <configOptions>
5 <java8>true</java8>
6 <dateLibrary>java8</dateLibrary>
7 <library>native</library>
8 <useJakartaEe>true</useJakartaEe>
9 <useBeanValidation>true</useBeanValidation>
10 <sourceFolder>.</sourceFolder>
11 <modelPackage>ch.schlau.pesche.apidocs.openapi.designfirst.generated.model</modelPackage>
12 </configOptions>
13 <additionalProperties>
14 <additionalProperty>supportUrlQuery=false</additionalProperty>
15 </additionalProperties>
16 <schemaMappings>
17 <schemaMapping>Pan=ch.schlau.pesche.apidocs.openapi.designfirst.txproc.model.Pan</schemaMapping>
18 </schemaMappings>
19</configuration>
- lines 3 and 7: with OpenAPI Generator the options to choose the programming language and flavour are
generatorName
andlibrary
- line 17: the option to map a schema from openapi.yaml to an existing Java class in the current version (openapi-generator 7.x) is
schemaMapping
(this was a breaking change from theimportMapping
used back with openapi-generator 4.x, and with 4.x it was even not always working: issue with 4.x) - line 14: another breaking change since 4.x: when using existing classes like
Pan
in my example, these existing classes are per default supposed to have aString toUrlQueryString(String prefix)
method. When they don’t, you need to providesupportUrlQuery=false
to avoid compilation errors.
Review of the Design First Approach
The integration of a code generator into the build workflow guarantees that the code always uses the current version of the OpenAPI documents.
There is however a small disadvantage (at least when using the Maven plugins): the plugins
always generate the code, even when the input (the openapi.yaml
file) didn’t change,
leading to unnecessary compilations.
And there is another small detail when using the MicroProfile /openapi
endpoint and having
the Design First openapi file baked into the application (e.g. having it in the
/src/main/resources/META-INF
directory):
- it’s not necessary that the MicroProfile implementation scans all the endpoints to generate
an OpenAPI document (as does the Code First approach) as the document already exists. This scan
can be disabled by setting the MicroProfile Config property
mp.openapi.scan.disable=true
- but this does not mean that the
/openapi
endpoint will serve exactly the provided file; as the MicroProfile implementation must be able to serve the document in both JSON and YAML formats, the implementation is still forced to parse (and convert) the provided OpenAPI document into the requested format (there might be some unimportant reordering of content in the OpenAPI document, while keeping the API equivalent).
Design First Gotchas
In my 2019 talk I mentioned the following gotchas:
Swagger Annotations Dependency
As mentioned above, code generated with Swagger Codegen relies on the
io.swagger.core.v3:swagger-annotations
dependency; this is still the case in 2024
OpenAPI Generator had the same issue back in 2019, but nowadays there is an
option (annotationLibrary
), and per default no dependency to any Swagger annotations is generated anymore.
Import Mapping
With OpenAPI Generator the mapping from a schema to an existing class didn’t work in 2019;
this has been fixed since, but the name of the option (schemaMapping
) has diverged from
Swagger Codegen, where the option is still named importMapping
.
supportUrlQuery
This a new gotcha in 2024 for OpenAPI Generator (see also the configuration example above).
When using schemaMapping
, the referenced classes are supposed to have a toUrlQueryString
method; when they don’t and the configuration supportUrlQuery=false
is missing you’ll
run into compilation errors.
Comparing the Approaches
It is no secret that I prefer the Design First approach, at least for applications that need maintenance (for quick prototyping and once-off experiments Code First is okay, if you need to document at all).
Disadvantages of Design First
- you have to learn how to write OpenAPI documents
- building the project is slower (run time of the generator, and the generator’s Maven plugins are dumb and always generate the code)
- when opening a freshly checked-out project in an IDE there are many missing classes because you need to generate them first
Advantages of Design First
- one source of truth: Code, Documentation and Annotations generated from the same information
- comparing interface versions is simple: just look at the difference of the OpenAPI documents (with Code First there can be hundreds of classes with OpenAPI annotations and you have to compare them all to know the differences)
- no documentation duplication and boilerplate, every structure/class/schema is documented only once
- easy integration with other tools working with OpenAPI documents
(API gateways, Developer Portals, Mocking and Testing Frameworks, etc).
With Code First you need some additional step to get at the OpenAPI document
(e.g. running the application and accessing the
/openapi
endpoint, or generating and copying the OpenAPI document out of the application jar where it was placed with a swagger maven plugin as shown in the first article)
Conclusion
I hope you have now a deeper understanding of the differences between the Code First and the Design First approaches.
If you enjoyed the two articles I’d like to hear from you about your own Code First or Design First preferences!