davidmoten / openapi-codegen

OpenAPI Java client and Spring server generator, strong typing, immutability, fail-fast validation, chained builders, feature rich
Apache License 2.0
8 stars 1 forks source link

openapi-codegen


codecov
Maven Central

Generates server-side and client-side Java classes of OpenAPI v3.0.3 (3.1 support coming bit-by-bit) using Jackson for serialization/deserialization, server-side targets Spring Boot. Born out of frustrations with openapi-generator and can be used standalone or in partnership with that project.

I suspect the future of this project will be to generate Java clients for APIs rather than server-side (except for one primary target that will be used for unit testing). The main reason for this is really the huge number of server-side frameworks that are out there. Yet to be decided!

Try it online here!

Features

Status: released to Maven Central

Limitations

Getting started

Working examples are at openapi-codegen-example-pet-store (client and server) and openapi-codegen-example-pet-store (client only).

Add this to your pom.xml in the build/plugins section:

<plugin>
    <groupId>com.github.davidmoten</groupId>
    <artifactId>openapi-codegen-maven-plugin</artifactId>
    <version>VERSION_HERE</version>
    <executions>
        <execution>
            <goals>
                <goal>generate</goal>
            </goals>
            <configuration>
                <basePackage>pet.store</basePackage>
            </configuration>
        </execution>
    </executions>
</plugin>
<plugin>
    <groupId>org.codehaus.mojo</groupId>
    <artifactId>build-helper-maven-plugin</artifactId>
    <version>3.4.0</version>
    <executions>
        <execution>
            <id>add-source</id>
            <phase>generate-sources</phase>
            <goals>
                <goal>add-source</goal>
            </goals>
            <configuration>
                <sources>
                    <source>${project.build.directory}/generated-sources/java</source>
                </sources>
            </configuration>
        </execution>
    </executions>
</plugin>

The example above generates java code from *.yml, *.yaml files in src/main/openapi directory.

We include build-helper-maven-plugin to help IDEs be aware that source generation is part of a Maven refresh in the IDE (for example in Eclipse Maven - Update project will run the codegen plugin and display the generated sources on the build path).

Here's an example showing more configuration options:

<plugin>
    <groupId>com.github.davidmoten</groupId>
    <artifactId>openapi-codegen-maven-plugin</artifactId>
    <version>VERSION_HERE</version>
    <executions>
        <execution>
            <goals>
                <goal>generate</goal>
            </goals>
            <configuration>
                <basePackage>pet.store</basePackage>
                <outputDirectory>${project.build.directory}/generated-sources/java</outputDirectory>
                <sources>
                    <directory>${project.basedir}/src/main/openapi</directory>
                    <includes>
                        <include>**/*.yml</include>
                    </includes>
                </sources>
                <failOnParseErrors>false</failOnParseErrors>
                <includeSchemas>
                    <includeSchema>Thing</includeSchema>
                </includeSchemas>
                <excludeSchemas>
                    <excludeSchema>Error</excludeSchema>
                </excludeSchemas>
                <mapIntegerToBigInteger>false</mapIntegerToBigInteger>
                <generator>spring2</generator>
                <generateService>true</generateService>
                <generateClient>true</generateClient>
            </configuration>
        </execution>
    </executions>
</plugin>

General advice

Generated code examples

Some examples follow. Note the following:

Schema classes

Note validations in constructors, private constructors for use with Jackson that wants nulls, public constructors that disallow nulls (use java.util.Optional), mandatory/optional fields, chained builder for maximal type-safety and readability, immutable mutator methods, generated hashCode, equals, toString methods.

oneOf with discriminator

Vehicle.java, Car.java, Bike.java

Note that discriminators are constants that the user does not set (in fact, cannot set) and are set in the private constructors of Car and Bike.

oneOf without discriminator

Geometry.java, Circle.java, Rectangle.java

anyOf without discriminator

anyOf is an interesting one, mainly because it is rarely used appropriately. In a review of 21 apis in [openapi-directory], 5 had valid use-cases for anyOf and the rest should have been oneOf. Using anyOf instead of oneOf will still support oneOf semantics but generated code will not give you as clean an experience (type-safety wise) than if oneOf had been used explicitly.

PetSearch.java, PetByAge.java, PetByType.java

AnyOfSerializer.java, PolymorphicDeserializer.java

allOf

Uses composition but also exposes all subschema properties at allOf class level (that delegate to subschema objects).

Dog3.java, Cat3.java, Pet3.java

    Pet3:
      type: object
      required:
        - petType
      properties:
        petType:
          type: string
    Dog3:
      allOf: # Combines the main `Pet3` schema with `Dog3`-specific properties
        - $ref: '#/components/schemas/Pet3'
        - type: object
          # all other properties specific to a `Dog3`
          properties:
            bark:
              type: boolean
            breed:
              type: string
              enum: [Dingo, Husky, Retriever, Shepherd]
    Cat3:
      allOf: # Combines the main `Pet` schema with `Cat`-specific properties
        - $ref: '#/components/schemas/Pet3'
        - type: object
          # all other properties specific to a `Cat3`
          properties:
            hunts:

Generated client

Here's an example of the generated client class (the entry point for interactions with the API). Note the conciseness and reliance on type-safe builders from a non-generated dependency.

Client.java

Generated Spring server-side classes

Immutability

All generated classes are immutable though List and Map implementations are up to the user (you can use mutable java platform implementations or another library's immutable implementations).

To modify one field (or more) of a generated schema object, use the with* methods. But remember, these are immutable classes, you must assign the result. For example:

Circle circle = Circle
    .latitude(Latitude.of(-10))
    .longitude(Longitude.of(140))
    .radiusNm(200)
    .build();
Circle circle2 = circle.withRadiusNm(250);

Builders

All generated schema classes have useful static builder methods. Note that mandatory fields are modelled using chained builders so that you get compile-time confirmation that they have been set (and you don't need to set the optional fields). Public constructors are also available if you prefer.

Here's an example (creating an instance of Geometry which was defined as oneOf:

Geometry g = Geometry.of(Circle
    .builder() 
    .lat(Latitude.of(-35f))
    .lon(Longitude.of(142f))
    .radiusNm(20)
    .build());

Note that if the first field is mandatory you can omit the builder() method call:

Geometry g = Geometry.of(Circle
    .lat(Latitude.of(-35f))
    .lon(Longitude.of(142f))
    .radiusNm(20)
    .build());

Validation

Enabled/disabled by setting a new Globals.config. Configurable on a class-by-class basis.

Nulls

The classes generated by openapi-codegen do not allow null parameters in public methods.

OpenAPI v3 allows the specification of fields with nullable set to true. When nullable is true for a property (like thing) then the following fragments must be distinguishable in serialization and deserialization:

{ "thing" : null }

and

{}

This is achieved using the special class JsonNullable from OpenAPITools. When you want an entry like "thing" : null to be preserved in json then pass JsonNullable.of(null). If you want the entry to be absent then pass JsonNullable.undefined.

For situations where nullable is false (the default) then pass java.util.Optional. The API itself will make this obvious.

Logging

slf4j is used for logging. Add the implementation of your choice.

Client

The generated client is used like so:

BearerAuthenticator authenticator = () -> "tokenthingy";
Client client = Client
     .basePath("https://myservice.com/api")
     .interceptor(authenticator)
     .build();

// make calls to the service methods:
Thing thing = client.thingGet("abc123");

Interceptors

Interceptors are specified in a client builder and allow the modification (method, url, headers) of all requests. An obvious application for an interceptor is authentication where you can add a Bearer token to every request.

Authentication

Set an interceptor in the client builder to an instance of BearerAuthenticator or BasicAuthenticator or do your own thing entirely.

HttpService

The HttpService can be set in the Client builder and encapsulates all HTTP interactions. The default HttpService is DefaultHttpService.INSTANCE which is based on HttpURLConnection class. Funnily enough the java HttpURLConnection classes don't support the HTTP PATCH verb. The default HttpService makes PATCH calls as POST calls with the header X-HTTP-Method-Override: PATCH which is understood by most web servers. If you'd like to use the PATCH verb then call .allowPatch() on the Client builder (for instance if you've modified HttpURLConnection static field using reflection to support PATCH).

The alternative to the default HttpService is ApacheHttpClientHttpService.INSTANCE which is based on Apache Httpclient 5.x (and has full support for the PATCH verb).

Multipart requests

Client code is generated for multipart/form-data requests specified in the openapi definition, including setting custom content types per part. Here's an example:

OpenAPI fragment:

paths:
  /upload:
    post:
      requestBody:
        content:
          multipart/form-data:
            schema:
              type: object
              properties:
                point:
                  $ref: '#/components/schemas/Point'
                description:
                  type: string
                document:
                  type: string
                  format: binary  
              required: [point, description, document]
            encoding:
              document:
                contentType: application/pdf
      responses:
        200:
          description: ok
          content:
            application/json: {}

Below is the generated type for the multipart/form-data submission object.

Here's the client code that uses it:

UploadPostRequestMultipartFormData upload = UploadPostRequestMultipartFormData
  .point(Point.lat(-23).lon(135).build()) 
  .description("theDescription") 
  .document(Document 
     .contentType(ContentType.APPLICATION_PDF) 
     .value(new byte[] { 1, 2, 3 }) 
     .build()) 
  .build();
client.uploadPost(upload);

readOnly and writeOnly

Sometimes you want to indicate that parts of an object are used only in a response or only in a request (but the core parts of the object might be used in both). That's where readOnly and writeOnly keywords come in.

If a field is marked readOnly

If a field is marked writeOnly

Marking a property as readOnly has the following effects on generated code:

Here's an example of generated code with readOnly fields: ReadOnly.java.

Marking a property as writeOnly has the following effects on generated code:

Here's an example of generated code with writeOnly fields: WriteOnly.java.

Server side generation

Ignoring paths for server side generation

Just add an extension to the OpenAPI file to indicate to the generator not to generate a server side method for a path:

paths:
  /upload:
    post:
      x-openapi-codegen-include-for-server-generation: false
      ...

An example of supplementing generated spring server with an HttpServlet is in these classes:

Mixed usage with openapi-generator

See this.

What about openapi-generator project?

This project openapi-codegen is born out of the insufficiences of openapi-generator. Great work by that team but VERY ambitious. That team is up against it, 37 target languages, 46 server frameworks, 200K lines of java code, 30K lines of templates. April 2023 there were 3,500 open issues (whew!).

So what's missing and what can we do about it? Quite understandably there is a simplified approach in openapi-generator code to minimize the work across many languages with varying capabilities. For Java this means a lot of hassles:

Testing

Lots of unit tests happening, always room for more.

Most of the code generation tests happen in openapi-codegen-maven-plugin-test module. Path related stuff goes into src/main/openapi/paths.yml and schema related stuff goes in to src/main/openapi/main.yml. Unit tests of generated classes form those yaml files are in src/test/java.

In addition to unit tests, openapi-codegen appears to generate valid classes for the following apis:

Docusign api needs work here because has more than 255 fields in an object which exceeds Java constructor limits.

To run tests on the above apis call this:

./test-all.sh 

This script ensures that the code generated from the above large test apis compiles and does so in many separate generation and compile steps because the apis generate so much code that the compilation step runs out of memory on my devices!

openapi-directory testing

If openapi-directory repository is cloned next to openapi-codegen in your workspace then the command below will test code generation on every 3.0 definition (>1800) in that repository. This command requires mvnd to be installed.

cd openapi-codegen-generator
./analyse.sh 

Output is written to ~/oc-TIMESTAMP.log

For convenience I add executables to /usr/local/bin with ./install-executables.sh. That way I can run codegen or codegenc from anywhere.

TODO