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!
Features
- Very clean minimal generated code (reused logic is in runtime libraries (server or client))
- Immutable generated schema classes (none of this mutable Java beans getters-and-setters rubbish)
- Extensively unit tested (and easy to add more to either demonstrate problems or correctness)
- Supports Java 8, 11, 17 (CI)
- Supports Spring Boot 2.x, 3.x server-side
- Supports
oneOf(discriminated/non-discriminated),anyOf(non-discriminated),allOf oneOfandanyOfvalidate on creationallOfgenerates an uber object with all members properties andasBlah()methods to access the individual members in a typed fashion- Nesting in openapi definition reflected in nested Java classes
- Generates chained builders (chaining occurs when mandatory fields are present). This makes checking setting of mandatory fields a compile-time check.
- Strong typing (primitives are used for mandatory simple types, chained builders)
- Java 8 Optional and DateTime types used
- Generates
equals,hashCode,toStringmethods - Plenty of unit tests (good ones, full serialization and deserialization tests)
- Maven plugin
- Simple server-side and client-side implementation for primary and general response handling
- Partial use of schema generated classes possible with generated server and client of openapi-generator-plugin
- Constructor validation of schema objects means fail-fast which helps with diagnosis
multipart/form-datarequest body support (client)form-urlencodedrequest body support (client)- individual requests can be customized with timeouts and extra headers
- use Java HttpsURLConnection for HTTP interactions or use Apache Httpclient 5.x (raise an issue to add another Http library)
Status: released to Maven Central
allOfonly with object schemas- parameter types like explode, label, deepObject not implemented yet
- multipart and form url encoded request bodied implemented on client, not server yet.
- security schemes not modelled (implement an
Interceptoror useBearerAuthenticatororBearerAuthenticator) - json only (xml not supported)
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>- As much as possible make sure you put your types in the
#/components/schemassection of your openapi yaml/json file (use$ref!). The same goes for responses, pathItems, and anything else that can be referred to with a$ref. Don't use anonymous types, it makes for an ugly experience with generated code. - Specify
format: int32on integers to ensure you end up withint/integertypes in generated code - Be sure to specify the properties that are mandatory (using
required:) - Set an
operationIdfield for every path entry to ensure you get sensible generated method names (in client and server) - always specify
mappingandpropertyNamefields for discriminatedoneOf - use OpenAPI 3.0 not 3.1 (the world is still working on tool support for 3.0 and is not ready for 3.1)
Some examples follow. Note the following:
- really clean code, formatted, sensible whitespacing, no long code lines
- minimal generated code (for example
toString,hashCode, and oneOf Deserializer are one statement methods that pass off to non-generated runtime dependencies) - type safety
- concise chained builders that check mandatory fields are set at compile-time
- constructor validation that can be configured off on a class by class basis
- Optional/JsonNullable should be used, not null values, in all public interactions
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.
- Book.java
- User.java
- Language.java (enum)
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.
Geometry.java, Circle.java, Rectangle.java
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
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: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.
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);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());Enabled/disabled by setting a new Globals.config. Configurable on a class-by-class basis.
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 the Jackson library. 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.
slf4j is used for logging. Add the implementation of your choice.
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 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.
Set an interceptor in the client builder to an instance of BearerAuthenticator or BasicAuthenticator or do your own thing entirely.
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).
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);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:
See this.
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:
- Mutable classes mean that validation cannot be performed at construction time and have to use validation-api annotations. Errors raised at serialization time not at object creation time so finding the cause of the error is problematic.
- Missing out on the many benefits of immutability (google for benefits of immutability)
- No support for oneOf, anyOf when no discriminator specified
- when discriminator mappings specified two sets of conflicting mapping annotations are generated
- SimpleRef case has no type safety (Ref is passed in as Object in constructor)
- unnecessary generated imports
- anonymous schemas generated as top level classes when could be nested static member classes (pollutes top level package)
- should be able to create oneOf member without specifying discriminator value in constructor (is constant)
- field types should be primitives in constructors, getters when mandatory (means a compile-time error instead of a runtime error)
- testing approach in the project lacks JSON serialization and deserialization tests at a unit level (as opposed to starting up servers and doing integration tests)
- import mapping is very poor, doesn't handle related objects and doesn't update service classes (non-model classes)
- a LOT of bugs (3,500 open issues is an indicator)
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:
- EBay
- Marqueta
- OpenFlow
- Spotify
- Google Chat
- Federal Electoral Commission
- BitBucket
- MailChimp
- GitHub
- OpenAI
- Atlassian JIRA
- Stripe
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!
, doneadditionalProperties(Dictionary) support- generate javadoc for fields
notsupportanyOfwith discriminator support- delegate constructors using
this( - workaround JsonCreator not being able to pass
5into a double argument, must be5.0(FasterXML/jackson-core#532) - document limited support for parameter style with spring-boot rest
- support objects with more than 255 fields (max parameter number in Java gets exceeded in object constructor)
- support form-style request bodies on server-side (multipart, urlencoded). Client side support done (not comprehensive).
- support more parameter styles
- support xml
- write docs
- support 3.1 features (type arrays, null instead of nullable, contentMediaType and contentEncoding for file payloads)