Swagger Codegen for Keytiles model files
Overview
Contains customized generator for https://github.com/swagger-api/swagger-codegen - based on the original JavaClientCodegen
Project skeleton was created following https://github.com/swagger-api/swagger-codegen/#making-your-own-codegen-modules instructions.
Licensing
This code comes with Apache License 2.0
Releases
You can check the CHANGELOG.md file for details.
Model generation
Global differences compared to standard Java rendering
The following are happening globally:
- by default we use
library=resttemplate
option (which is using Jackson library style) - @Schema, @ApiModel, @ApiModelProperty and similar annotations are removed from generated model classes - so dependency
io.swagger.core.v3:swagger-annotations
is not needed anymore to use the model classes - @javax.* annotations are also removed from generated model classes
serializableModel
is turned on by default (model classes are implementingSerializable
interface). But you can overcome this with adding optionserializableModel=false
ignoreImportMapping
option is defaulted tofalse
- at least in the beginning... (see my comment: swagger-api/swagger-codegen#10419 (comment)) But it is enough to get the option working and collect models. Of course this would not block generate models we dont want/need so optionexcludeImportMappingsFromGeneration
is also introduced.- several extra OpenApi
x-
tags are available to drive model generation on a "hint" basis - see section OpenApi 'x-' tags for more details! - added support to use primitive types in models instead of always using wrapper classes - see option usePrimitiveTypesIfPossible
option 'modelStyle'
An option named modelStyle
is available to fine tune model rendering style. Using this is optional.
note: If you are using the swagger-codegen-maven-plugin you can add this to <configOptions>
section!
value 'inherited'
In this case not much happens compared to the underlying Java rendering just what is already documented above in the 'Global differences compared to standard Java rendering' section
value 'simpleConsistent' (default)
With this model generation style we get a more straightforward model. And markers like required
, readOnly: true
fields
become more important. Also the nullable
and default
declarations.
The main goal is: to get a simplified (no unnecessary setters/getters) but enforced consistency (instantiation of a model class via non-zero argument constructor) in the generated models.
This approach brings closer the written code (which is working with the models) with the defined OpenApi contracts and while we are writing the code - thanks to the enforced consistency - we might get into situations more likely we realize the model is not good enough / not behaves as expected and contract should be modified / fine tuned maybe? Also when we change the contract and regenerate the models it might lead to compile errors quickly - showing clearly which parts the current code (working with the models) needs adjustments.
Here are the rules this generation follows:
- If an Object property is marked as
required
orreadOnly: true
then this will become aprivate final
field of the model (and will be enforced get value through Constructor)
Why 'private final'? Why 'final'? You might ask... Because this is a POJO model thus it does not have any internal logic which could change values after instantiation. Of course the code which builds the model might want to change such field multiple times in the process of generation BEFORE releasing the POJO out (for readers) but this is actually the area of Builder Pattern right? - If a property is
nullable: false
then it will become aprivate
field with a setter() method protecting against setting NULL value to it later, furthermore:- if it has a
default: something
value then value in schema then will be assigned on the field level as a start.
note: same happens if field however does not have a default value on schema level but there is a logical default alternative, e.g. if the field is a List<> we can assign an empty ArrayList, similarly we can do with Sets or Maps, etc. - but if we can not figure out anyhow (see above) a meaningful non-NULL default value then field will be part of the Constructor so a non-NULL value will be enforced
- if it has a
- Other Object properties will become simply a
public
field - so no setter/getter is needed there - Constructor. If a model has
a) anyprivate final
field (see #1) OR
b) anyprivate
field (which isnullable: false
) without a meaningful default value (see #2) OR
c) has a Superclass which already has a non-zero argument constructor
then the model will get a non-zero argument constructor. (The zero-argument constructor goes away in this case)
To put it other words: to instantiate the model class you need to conform with the property level contract. - The builder style property setter methods are removed in this model generation (it would have been too complicated).
The name of the generation "simple" and "consistent" comes from the above rules which:
- guarantee strong(er) consistency between schema contract and model usage from code
- by introducing making fields public whenever possible AND removing every non-absolutely-necessary @annotations from the model are simplifying the model / dependencies you need to pull in heavily
Please note! As a consequence of the above rules if you declare all properties in an Object in the schema readOnly: true then basically an immutable model will be generated... (note: again a nice future feature could be to support builder generation too for these or similar cases)
option 'keepPropertyNames'
This is a boolean option. Default: false
The original Java rendering is transforming field names into java style camel case. Which is in most of the cases totally fine however sometimes is screwing up model readability... e.g. a field named 'g_tId' is changed to 'gTId'.
If you don't want that and you are writing your OpenApi schema due to Java naming standards already you might want this not happening. In this case you can use this option - which is a boolean option
note: You also have the possibility to control this behavior on Object or even on Property level in the schema using the
x-keytiles-keep-property-names
/ x-keytiles-keep-property-name
flags!
option 'nullableTagDefaultValue'
This is a boolean option. Default: false (why? read!)
OpenApi spec (it looks) is assuming every field nullable=false
if the nullable
attribute is not given.
This would enforce the modelStyle=simple
to literally make every field private where you do not write explicitly
nullable=true
. Which is not necessarily what you want in that rendering style...
So this option was introduced to redefine the default assumption if this attribute is not specified.
Of course the default of this is 'FALSE' to conform with standard and compatibility. But you can override this.
option 'addExplanationsToModel'
This is a boolean option. Default: false
This is a very useful debugging feature! The generator in modelType=simpleConsistent generation is making decisions based on complex rules. And it might not be obvious e.g. while a property became private
or private final
field? Then turn this on!
This will add // @Generator
comments to model class fields, methods and constructor to understand better why the generator made decisions.
option 'usePrimitiveTypesIfPossible'
This is a boolean option. Default: false
If set to true then this gives a "strong wish" to the generator it should try to use primitive datatypes whenever it is possible. After this point the generator will follow certain rules and considerations to achieve this.
Property datatypes - using primitive types?
The original Java rendering is always using wrapper types (Integer, Long, Boolean, etc). I guess this is because this way we do not need to face with heavy decisions. Using wrapper types vs primitive types has definitely several impacts e.g. handling default values or 'is nullable?' questions...
However using the wrapper types definitely has consequences with memory footprint and speed. It's typically not an issue with majority of the model cases but sometimes it is.
This article sums up things pretty well: https://www.baeldung.com/java-primitives-vs-objects
In Keytiles we do have use cases where we do not want wrapper types in the model for sure! Therefore this code generator offers control over this. We have to... :-)
Another fact is that approach the topic 'automagically' is difficult as eventually only the author of the schema might know the perfect answer whether which way is the best and this might vary even from property to property...
Rules/considerations the generator will follow:
- In first step we check if it would be possible - at all - to use primitive datatype. This means:
- The type of the property is eligible to do so, so it is int, long, bool, double, float etc. Obviously complex types like Array or Object or even String is not an option AND
- property is
nullable=true
This is important as otherwise we need to do NULL checks which is a not defined operation on primitive types
- In this second step we chek if we were asked anyhow to use primitive type, namely:
- if
x-keytiles-use-primitive-datatype=false
is explicitly set on the property then we exit - we will not use primitive type EOP - if
x-keytiles-use-primitive-datatype=true
is explicitly set on the property then it is an explicit instruction to do so. If we can use primitive type (step #1 evaluated to true) then we will. Otherwise generator exits with an error message as we can not proceed with this contradicting information. - if
x-keytiles-use-primitive-datatypes-if-possible=true
is set on object level the property belongs to OR
if optionusePrimitiveTypesIfPossible=true
AND we can use primitive type (step #1 evaluated to true) then we will. Otheriwse we simply will not and go with wrapper type - as normally we would do.
- if
option 'addSchemaModelsToImportMappings'
This is a comma separated list of setups - which are reading up OpenApi schema files and adding models found in them to importMappings
.
Let us explain through an example why it is useful!
Let's assume you have two (or more) OpenApi schema files:
- base-types.yaml - declaring some objects, and
- my-something-api.yaml which is using objects declared in 'base-types.yaml' referring them in with
$ref: base-types.yaml#/components/schemas/...
.
In this case if you generate models from 'base-types.yaml' into package 'myapis.base' (one command line or maven plugin execution) and then you also generate models from 'my-something-api.yaml' into package 'myapis.something' (another command line or maven plugin execution) actually what you will see is classes defined in 'base-types.yaml' and referred in into 'my-something-api.yaml' will be also generated again into package 'myapis.something'.
So how can I reuse the base classes then?
OK here comes importMappings
option to the play. Right? When you are generating 'my-something-api.yaml' you can add every class declared in 'base-types.yaml' into importMappings so they will be reused. Cool!
But there is a problem... If you add new classes to 'base-types.yaml' then you also need to adjust the importMappings. Manually... Same happens if you drop objects from 'base-types.yaml'.
Here comes the addSchemaModelsToImportMappings
option very handy!
You could write something like this:
Set addSchemaModelsToImportMappings
to base-types.yaml::modelPackage=myapis.base
when you are generating 'my-something-api.yaml'. Bingo! This way during the generation the schema 'base-types.yaml' will be read and objects in it will be scanned. Then - using the 'modelPackage' part we construct the importMappings automatically. So manual maintenance of importMappings is no longer needed during generating 'my-something-api.yaml'
You can take a look below how to use it in Maven plugin - using it on CLI is very similar.
Please note! If you add more schemas in this option separated with ,
then keep an eye on the order! It matters! Because the generator will iterate through the schemas in the given order and build the importMappings
the way a model (object) is just added to it if it is not in the importMappings yet!
option 'excludeImportMappingsFromGeneration'
This is a boolean option. Default: true
This option helps to overcome the problem we described in the global differences, 'ignoreImportMapping' bullet.
The effect of this option is what 'ignoreImportMapping' meant to do but the difference is that this option just stepping in at the very very last step of the generation process: the generation (from Mustache templates) itself.
It is recommended to use this option as it is completely harmless until you really go into a situation we described at option 'addSchemaModelsToImportMappings'
option 'mavenExecutionId'
This is a string option. Unique ID of the execution (from Maven plugin)
Using this option only makes sense if
- You are generating via Maven plugin
- You want to add result of generation to another Maven execution as
importMappings
Otherwise just leave this option out!
note: You can take a look below how to use it in Maven plugin to see an example
option 'addSchemaModelsToImportMappingsFromMavenExecutions'
This is a string option, comma separated list of Maven executionIds.
This option is basically doing the same as option 'addSchemaModelsToImportMappings' but more simple as it is just referring back to earlier running (so preceeding) Maven executions via option 'mavenExecutionId'.
note: You can take a look below how to use it in Maven plugin to see an example
Usage
In Java, Maven - generating models
However this altered version is introducing a new language KeytilesJava
(instead of using java
) it will not work
in the swagger-codegen-maven-plugin config... As to get the language based detection working there should be a file added
to the resources /META-INF/services/io.swagger.codegen.v3.CodegenConfig
listing the KeytilesJavaCodegen
class... But I decided not to add this as then it would override the default one comes from original swagger-codegen .jar project making all
out-of-the-box supported generators disappear. And this is not good... (you find this file saved away into /parked-resources folder)
Actually (as turned out by debugging) the plugin ... spec also supports a fully qualified class name. So, here is a plugin example how you should use it:
To let Maven download this project you need to add this to your pom.xml
:
To the main section add:
<pluginRepositories>
<pluginRepository>
<id>keytiles-public-releases</id>
<name>Keytiles Nexus Public Releases repo</name>
<url>https://nexus.keytiles.com/nexus/content/repositories/public-releases/</url>
</pluginRepository>
<pluginRepository>
<id>keytiles-public-snapshots</id>
<name>Keytiles Nexus Public Snapshots repo</name>
<url>https://nexus.keytiles.com/nexus/content/repositories/public-snapshots/</url>
</pluginRepository>
</pluginRepositories>
and then to the plugins section:
<plugin>
<groupId>io.swagger.codegen.v3</groupId>
<artifactId>swagger-codegen-maven-plugin</artifactId>
<version>${maven.swagger-codegen.plugin}</version>
<dependencies>
<dependency>
<groupId>com.keytiles</groupId>
<artifactId>keytiles-swagger-codegen</artifactId>
<version>${keytiles.codegen.version}</version>
</dependency>
</dependencies>
<executions>
<!-- this execution generates from 'common-types-v2.yaml' -->
<execution>
<id>common</id>
<phase>generate-sources</phase>
<goals>
<goal>generate</goal>
</goals>
<configuration>
<inputSpec>${project.basedir}/src/main/openapi/common-types-v2.yaml</inputSpec>
<output>${project.basedir}/target/generated-sources/swagger/</output>
<modelPackage>com.keytiles.api.model.common.v2</modelPackage>
<language>com.keytiles.swagger.codegen.KeytilesJavaCodegen</language>
<!-- <library>resttemplate</library> -->
<generateApis>false</generateApis>
<generateModels>true</generateModels>
<generateApiTests>false</generateApiTests>
<generateApiDocumentation>false</generateApiDocumentation>
<generateModelTests>false</generateModelTests>
<generateModelDocumentation>false</generateModelDocumentation>
<generateSupportingFiles>false</generateSupportingFiles>
<configOptions>
<!-- this was we will use the original Java generation style -->
<modelStyle>inherited</modelStyle>
<interfaceOnly>true</interfaceOnly>
<dateLibrary>java8</dateLibrary>
</configOptions>
</configuration>
</execution>
<!-- this execution generates from 'stat-api-v2.yaml' -->
<execution>
<id>stat-api-v2</id>
<phase>generate-sources</phase>
<goals>
<goal>generate</goal>
</goals>
<configuration>
<inputSpec>${project.basedir}/src/main/openapi/proposal/stat-api-v2.yaml</inputSpec>
<output>${project.basedir}/target/generated-sources/swagger/</output>
<importMappings>
<importMapping>BaseResponseClass=com.keytiles.api.model.common.v2.BaseResponseClass</importMapping>
<importMapping>ContainerQueryRangeResponseClass=com.keytiles.api.model.common.v2.ContainerQueryRangeResponseClass</importMapping>
<importMapping>TileTypeEnum=com.keytiles.api.model.common.v2.TileTypeEnum</importMapping>
</importMappings>
<modelPackage>com.keytiles.api.model.stat.v2</modelPackage>
<!-- <templateDirectory>${project.basedir}/templates/Java/<templateDirectory> -->
<language>com.keytiles.swagger.codegen.KeytilesJavaCodegen</language>
<!-- <library>resttemplate</library> -->
<generateApis>false</generateApis>
<generateModels>true</generateModels>
<instantiationTypes></instantiationTypes>
<generateApiTests>false</generateApiTests>
<generateApiDocumentation>false</generateApiDocumentation>
<generateModelTests>false</generateModelTests>
<generateModelDocumentation>false</generateModelDocumentation>
<generateSupportingFiles>false</generateSupportingFiles>
<configOptions>
<!-- here we use the 'simpleConsistent' generation style -->
<modelStyle>simpleConsistent</modelStyle>
<nullableTagDefaultValue>true</nullableTagDefaultValue>
<!-- warning! if you add more then order really matters here! see option description! -->
<addSchemaModelsToImportMappings>
${project.basedir}/src/main/openapi/common-types-v2.yaml::modelPackage=com.keytiles.api.model.common.v2
</addSchemaModelsToImportMappings>
<!-- default: true -->
<!-- excludeImportMappingsFromGeneration>false</excludeImportMappingsFromGeneration> -->
<!-- for debug purposes you can use the below - just set it 'true' -->
<addExplanationsToModel>false</addExplanationsToModel>
<interfaceOnly>true</interfaceOnly>
<dateLibrary>java8</dateLibrary>
</configOptions>
</configuration>
</execution>
</executions>
</plugin>
Templates
For Java target
Please see: README.md in templates resource folder !
If you just need to slightly alter model rendering on template level you can do that! The templateDirectory
option provided by underlying Swagger Codegen tooling is a possibility for you here too so you can copy and customize. See: https://github.com/swagger-api/swagger-codegen#modifying-the-client-library-format for more details!
Additional template variables
To render the model there might be additional variables available in the template. This section documents them.
modelStyle 'inherited'
There are no additional template variables in this style.
modelStyle 'simpleConsistent'
The following extra variables are available:
- needsConstructor: boolean flag, true if the model class needs a non-zero argument constructor (instead of default constructor). See Rules/#4!
- privateFinalFields: list of CodegenProperty objects (like in {#vars}) which are 'private final' fields of the model. See Rules/#1
note: if needsConstructor is true then you also need to take these arguments there and assign to the fields! - privateFields: list of CodegenProperty objects (like in {#vars}) which are 'private' fields of the model. See Rules/#2
- publicFields: list of CodegenProperty objects (like in {#vars}) which are 'public' fields of the model. See Rules/#3
- constructorSuperArgs: if we need a constructor (see needsConstructor) then list of CodegenProperty objects (like in {#vars}) which we take as arguments because we need to pass them to our Superclass constructor. If needsConstructor=false then or no such arguments needed then this list is empty.
- constructorNonNullablePrivateArgs: if we need a constructor (see needsConstructor) then list of CodegenProperty objects (like in {#vars}) which we take as arguments because we have to ensure they get a non-null value assigned. See Rules/#2/2
- constructorValidateNonNullArgs: if we need a constructor (see needsConstructor) then list of CodegenProperty objects (like in {#vars}) which the constructor must validate being non-null (this can include arguments from other arrays)
- constructorCombinedArgs: if we need a constructor (see needsConstructor) then this is a simple string which contains concatenation of constructorSuperArgs + privateFinalFields + constructorNonNullablePrivateArgs properties and rendered as a comma separated list of arguments. Just as you would write the arguments in Java code.
OpenApi 'x-' tags
In the OpenApi schema we have defined a few extension tags this generator supports. For readability and distinguish purposes they all have a x-keytiles-
prefix.
Availability might depend on the used modelStyle
(see option documentation) so before you use any check if your model style is considering them or not
With the names we try to be concrete language agnostic (e.g. Java or Javascript or Python etc) and instead we try to be more "hints" based - as much as possible. So a name of an extension tag is expressing a meaning of something.
Please also note that
x-keytiles-serialize-only-if-non-default
data type: boolean
default: false
supported in modelStyles: all
supported in languages/libraries: KeytilesJava/Jackson based libraries
applicable on elements: object properties
It becomes important if model object is serialized e.g. into a JSON String (or similar). If this flag is true then it tells the serialization logic to omit this property from the output if its value is the (data type dependent) default value.
E.g. in Jackson json serialization this will annotate the property with @JsonInclude(Include.NON_DEFAULT)
annotation
x-keytiles-keep-property-names
data type: boolean
default: inherited from above
supported in modelStyles: all
supported in languages/libraries: all
applicable on elements: object
If this is present then it is overriding against global option keepPropertyNames
(see option documentation) for all properties defined in this object. Otherwise value of keepPropertyNames
option is inherited.
x-keytiles-keep-property-name
data type: boolean
default: inherited from above
supported in modelStyles: all
supported in languages/libraries: all
applicable on elements: object properties
If this is present then it is overriding against global option keepPropertyNames
(see option documentation) for more details!) or against x-keytiles-keep-property-names
defined on parent object. Otherwise value is inherited from above.
x-keytiles-use-primitive-datatypes-if-possible
data type: boolean
default: inherited from option 'usePrimitiveTypesIfPossible'
supported in modelStyles: all
supported in languages/libraries: all
applicable on elements: objects
See section Property datatypes - using primitive types? for more details
x-keytiles-use-primitive-datatype
data type: boolean
default: inherited from option 'usePrimitiveTypesIfPossible'
supported in modelStyles: all
supported in languages/libraries: all
applicable on elements: object properties
See section Property datatypes - using primitive types? for more details