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
oneOf
(discriminated/non-discriminated), anyOf
(non-discriminated), allOf
oneOf
and anyOf
validate on creationallOf
generates an uber object with all members properties and asBlah()
methods to access the individual members in a typed fashionequals
, hashCode
, toString
methodsmultipart/form-data
request body support (client)form-urlencoded
request body support (client)Status: released to Maven Central
allOf
only with object schemasInterceptor
or use BearerAuthenticator
or BearerAuthenticator
)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>
#/components/schemas
section 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.format: int32
on integers to ensure you end up with int/integer
types in generated coderequired:
)operationId
field for every path entry to ensure you get sensible generated method names (in client and server)mapping
and propertyName
fields for discriminated oneOf
(but prefer non-discriminated oneOf
)Some examples follow. Note the following:
toString
, hashCode
, and oneOf Deserializer are one statement methods that pass off to non-generated runtime dependencies)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.
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 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.
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);
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:
Optional
Optional.empty
to be passed Optional.empty
(null or absent) is passed readOnly
field (
it is only at deserialization time that we enforce a required property)Here's an example of generated code with readOnly
fields: ReadOnly.java.
Marking a property as writeOnly
has the following effects on generated code:
Optional
Optional.empty
to be passed Optional.empty
(null or absent) is passed writeOnly
field (
it is only at serialization time that we enforce a required property)Here's an example of generated code with writeOnly
fields: WriteOnly.java.
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:
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!
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.
additionalProperties
(Dictionary) supportnot
supportanyOf
with discriminator supportthis(
5
into a double argument, must be 5.0
(https://github.com/FasterXML/jackson-core/issues/532)