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:

Design First: the code is generated from the OpenAPI document Design First: the code is generated from the OpenAPI document

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 the library 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).

OpenAPI schema for PinCheckRequest
  • design_first
  • code_first_swagger
  • code_first_quarkus
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
PinCheckRequest:
  description: Request for checking a PIN
  required:
  - pan
  - pinBlock
  - uuid
  type: object
  properties:
    uuid:
      description: Unique ID of the request
      format: uuid
      type: string
    pan:
      $ref: '#/components/schemas/Pan'
    pinBlock:
      description: |-
        Encrypted binary data containing a PIN

        Fieldcode: C003
      type: string

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 to io.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 the Pan type from the first article that was using type=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 (and JsonInclude.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 the Pan type from the first article that was using type=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 and library
  • 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 and library
  • 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 the importMapping 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 a String toUrlQueryString(String prefix) method. When they don’t, you need to provide supportUrlQuery=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!

Peter Steiner

Software Developer and Opinionated Citizen

Switzerland