fabrikt
- Introduction
- Features
- Examples
- Usage Instructions
- Getting the Most from Fabrikt
- Configuration Options
- Migration Guide
- Original Motivation
- Building Locally
- Publishing
- Specific Features
Introduction
This library was built to take advantage of the complex modeling features available in OpenAPI 3. It generates Kotlin data classes with advanced support for features such as:
- Null Safety
- Inlined schema definitions
- Enumerations
- Sealed Classes
- Polymorphism (
@JsonSubTypes) - Maps of Maps of Maps
- GraalVM Native Reflection Registration
- Json Merge Patch (via
JsonNullable) (addx-json-merge-patch: trueto schemas) - Override Jackson Include NonNull (via
JsonInclude) (addx-jackson-include-non-null: trueto schemas) - Jackson's polymorphic subtype deduction (add
x-jackson-subtype-deduction: trueto theoneOfschema) to gain Sealed Interface polymorphism, without needing to declare a discriminator property. Requires subtypes to have distinguishing required fields.
as well as HTTP clients and controllers for a number of popular frameworks (see Features).
Fabrikt also generates models directly from a plain JSON Schema document — draft-04 through 2020-12, optionally nested inside a larger resource such as a Nakadi EventType manifest — with no OpenAPI spec required. See Generating from a JSON Schema document.
More than just bootstrapping, this library can be permanently integrated into your build tool and will ensure contract and code always match, even as APIs evolve in complexity.
Try Fabrikt Online
Try Fabrikt with your own API spec in the Fabrikt Playground and see how it can help you generate code for your API clients and servers.
Coordinates
<dependency>
<groupId>io.fabrikt</groupId>
<artifactId>fabrikt</artifactId>
</dependency>
Features
The library currently has support for generating:
- Models
- Jackson annotated data classes
- Kotlinx.serialization annotated data classes
- Clients
- OkHttp Client (w/ Jackson Models)
- OpenFeign annotated client interfaces
- Ktor Client (w/ Jackson & Kotlin Serialization models)
- Spring HTTP Interface annotated client interfaces
- Controllers
- Spring MVC annotated controller interfaces
- Micronaut HTTP annotated controller interfaces
- Ktor server routes and controller interfaces
Client generators offer opt-in header API-key, HTTP Bearer and OAuth2 helpers through --http-client-opts OPENAPI_API_KEY_AUTHENTICATION, OPENAPI_BEARER_AUTHENTICATION and OPENAPI_OAUTH2_AUTHENTICATION. See Client authentication for credential providers, operation scopes, security alternatives and examples.
Examples
Consult the test directory for OpenAPI code generation examples.
It forms a living documentation full of code examples generated from different OpenAPI 3 permutations.
Furthermore, the end-to-end tests demonstrate how to integrate the library using Gradle.
Usage Instructions
The library can be used in a variety of ways, including as a command line tool, a Gradle task, or a Maven plugin.
Please refer to Configuration Options section for a list of available parameters.
Command Line
Fabrikt is packaged as an executable jar, allowing it to be integrated into any build tool.
The CLI can be invoked as follows:
java -jar fabrikt.jar \
--output-directory '/tmp' \
--base-package 'com.example' \
--api-file '/path-to-api/open-api.yaml' \
--targets 'client' \
--targets 'http_models'
--api-file and --api-fragment also accept a resolvable http:///https:// URL instead of a local path, in which case Fabrikt fetches the spec at generation time.
Use --auth to send headers on those fetches and on any remote $ref the parser resolves; values can contain ${ENV_VAR} placeholders, name an env var, or contain !cmd (at start or after whitespace) to run a shell command and substitute its trimmed stdout (e.g. --auth 'Authorization: Bearer !generate-fresh-auth-token').
The !cmd form requires a POSIX sh on the PATH and is not available on plain Windows.
Only fetch specs from URLs you trust — pointing Fabrikt at an untrusted or attacker-influenced URL carries the same risk as fetching any other untrusted network resource.
Generating from a JSON Schema document
--json-schema-file takes a JSON Schema document (draft-04 through 2020-12) as the primary input instead of --api-file, converting it to an OpenAPI 3.1 document before generation runs.
A trailing #/json/pointer fragment (RFC 6901) selects the schema nested inside a larger resource, such as a Nakadi EventType manifest's spec.schemaObject.
--json-schema-root-name names the generated root schema; it defaults to the schema's title, then the resource's /metadata/name, then Schema with a warning if neither is present.
java -jar fabrikt.jar \
--json-schema-file 'manifest.yaml#/spec/schemaObject' \
--json-schema-root-name 'OffersConfig' \
--base-package 'com.example' \
--targets 'http_models'
--json-schema-file with no fragment, or an empty fragment (manifest.yaml#), treats the whole file as a bare JSON Schema document.
The converter hoists definitions/$defs (and any #/components/schemas/* siblings already referenced in an OpenAPI-shaped source document, such as AsyncAPI) into components/schemas, rewriting and validating every $ref.
It upgrades draft-04 boolean exclusive bounds to the numeric 2020-12 form, and promotes a top-level {type: string, examples: [...]} definition to x-extensible-enum.
Keywords with no OpenAPI equivalent — if/then/else, not, patternProperties, dependencies, unevaluated*, prefixItems — and constraint-only oneOf/anyOf/allOf branches are dropped rather than approximated.
Kubernetes CRD conversion is not a supported target.
Tip: You can also run the latest version without a manual download via JBang:
jbang run io.fabrikt:fabrikt:RELEASE [args...]
Gradle w/ custom task
Here is an example of a Gradle task with code generated to the build/generated directory, and execution linked to the compile task.
val fabrikt: Configuration by configurations.creating
val generationDir = "$buildDir/generated"
val apiFile = "$buildDir/path-to-api/open-api.yaml"
sourceSets {
main { java.srcDirs("$generationDir/src/main/kotlin") }
test { java.srcDirs("$generationDir/src/test/kotlin") }
...
}
tasks {
...
val generateCode by creating(JavaExec::class) {
inputs.files(apiFile)
outputs.dir(generationDir)
outputs.cacheIf { true }
classpath(fabrikt)
mainClass.set("io.fabrikt.cli.CodeGen")
args = listOf(
"--output-directory", generationDir,
"--base-package", "com.example",
"--api-file", apiFile,
"--targets", "http_models",
"--targets", "client",
)
}
withType<org.jetbrains.kotlin.gradle.tasks.KotlinCompile> {
kotlinOptions.jvmTarget = "17"
dependsOn(generateCode)
}
}
dependencies {
fabrikt("io.fabrikt:fabrikt:+") // This should be pinned
...
}
Gradle w/ plugin
The Fabrikt Gradle plugin serves as a convenient wrapper for Fabrikt, allowing seamless integration of code generation into a Gradle build.
Note: Since the plugin is maintained separately from the Fabrikt library, please refer to the Configuration section of the plugin's README for the most up-to-date information on how to use it.
plugins {
// find latest version: https://github.com/acanda/fabrikt-gradle-plugin/releases
id("ch.acanda.gradle.fabrikt") version "1.45.0"
}
fabrikt {
generate("dog") {
apiFile = file("src/main/openapi/dog.yaml")
basePackage = "com.example.api"
}
}
Maven
The official Fabrikt Maven plugin integrates Fabrikt code generation into Maven builds. Plugin releases use the same version as the corresponding Fabrikt release and support shared configuration, multiple OpenAPI specifications through named executions, generated source registration, and both lifecycle-bound and explicit generation. Refer to the plugin's usage documentation for the current configuration syntax and examples.
Alternatively, the exec-maven-plugin can download the Fabrikt library from Maven Central and execute its main method with defined arguments.
Docker
Fabrikt is also available as a Docker image, which can be convenient for CI/CD pipelines or environments where you prefer not to install Java directly.
The Docker image can be invoked as follows:
docker run --rm -v $(pwd):/workspace ghcr.io/fabrikt-io/fabrikt:latest \
--output-directory '.' \
--base-package 'com.example' \
--api-file 'openapi.yaml' \
--targets 'http_models'
The command mounts your current directory to /workspace in the container, where Fabrikt will read the OpenAPI specification and write the generated code.
Getting the Most from Fabrikt
1. Prefer components to inline schemas
Fabrikt generates models for inline objects in operation parameters, non-multipart request bodies, and responses, including supported allOf object shapes.
Other inline schema shapes can still fall back to generic types. For complex reusable schemas, define them under components in the OpenAPI spec. See PR #713 for the supported inline object cases.
2. Use oneOf with discriminator for polymorphism
oneOf generates polymorphic models with sealed interfaces.
The discriminator property is used by Fabrikt to determine the subtypes to be generated.
3. Add metadata to references in OpenAPI 3.0
OpenAPI 3.0 ignores extra fields beside a direct $ref. Wrap a referenced enum in a single-element allOf to give a property its own default or description; Fabrikt retains the referenced enum type and generates an enum constant for a matching default value.
4. OpenAPI 3.1 type arrays
A required property with type: [string, 'null'] generates String?.
When a schema declares multiple non-null types, Fabrikt logs a warning and generates Any with Jackson or JsonElement with kotlinx.serialization instead of a dedicated union model.
Use a supported oneOf schema when a concrete polymorphic Kotlin model is needed.
5. Read shared fields from explicitly composed models
Enable --http-model-opts SHARED_COMPOSITION_CONTRACTS to generate shared interfaces for compatible models that explicitly include a named object through allOf. See Shared composition contracts for examples and current scope.
Multipart clients
For OK_HTTP, OPEN_FEIGN and SPRING_HTTP_INTERFACE, multipart/form-data JSON object and array parts are sent as application/json, including when this is inferred from the schema rather than explicitly declared in encoding. Optional null parts are omitted; binary arrays are sent as repeated parts with the original property name. JSON parts do not receive a filename. Unsupported JSON-part encodings, such as application/xml, produce a warning and fall back to JSON; this does not add XML serialization or general support for multipart encoding options.
OkHttp
JSON parts use the client instance's configured Jackson mapper. File arguments use the generated RequestBodyWithFilename, retaining the caller's body, content type and filename.
OpenFeign
Multipart methods use native Feign @Param bindings and require the generated FeignMultipartEncoder. Register it on the Feign builder with your existing encoder and JSON serializer, for example FeignMultipartEncoder(JacksonEncoder(mapper), mapper::writeValueAsBytes) for Jackson 2. For Jackson 3, pass your Jackson 3 serializer and the encoder you use for ordinary requests. The multipart encoder delegates ordinary requests unchanged and serializes immutable Kotlin JSON models directly; a plain form encoder is insufficient. It buffers each multipart request in memory and requires no dependency beyond Feign core and the caller's existing JSON serializer.
JSON arguments retain their generated model types. File arguments use FeignMultipartFile(bytes, filename, contentType); binary arrays use List<FeignMultipartFile>. This changes the signatures of previously unsupported multipart methods. The generated interfaces use Feign's native contract; adding @FeignClient does not switch those bindings to Spring Cloud's contract.
Spring HTTP Interface
Parts use @RequestPart with their original names and requiredness. Wrap JSON arguments in the generated JsonPart(model) or JsonPart(listOfModels), which fixes their content type to JSON and preserves an array as one JSON part. File arguments use Spring's Resource, with a filename supplied by the resource; binary arrays use List<Resource>. These replace the previously unbound multipart parameters and therefore change their Kotlin signatures.
Configure a multipart-capable message converter, such as AllEncompassingFormHttpMessageConverter, with the selected Jackson generation's JSON part converter. A standalone JSON converter cannot serialize the complete multipart request. For a ByteArrayResource, override getFilename() to supply the upload filename. The synchronous RestClient adapter is supported; reactive adapter behavior must be verified separately.
Configuration Options
This section documents the available CLI parameters for controlling what gets generated. This documentation is generated using: ./gradlew printCodeGenUsage
To add an annotation to every generated HTTP model type, repeat --http-model-additional-annotations with a fully qualified class name, for example --http-model-additional-annotations com.example.DiffBuilder. The annotation must need no arguments and be available to the project compiling the generated code. Without this option, generated models are unchanged.
Usage:
| Parameter | Description |
|---|---|
--api-file |
This must be a valid Open API v3 spec. All code generation will be based off this input. Accepts either a local file path or a resolvable http(s) URL. |
--api-fragment |
A partial Open API v3 fragment, to be combined with the primary API for code generation purposes. Accepts either a local file path or a resolvable http(s) URL. |
--auth |
Authorization header(s) sent when fetching a remote --api-file, --api-fragment, or remote $ref. Repeatable, format 'Name: value'. Value may contain ${ENV_VAR} placeholders, name an env var, or contain !cmd (at start or after whitespace) to run a shell command and substitute its trimmed stdout (e.g. --auth "Authorization: Bearer !generate-fresh-auth-token"). |
* --base-package |
The base package which all code will be generated under. |
--custom-type-mapping |
Map an OpenAPI type and format to a Kotlin type. Use type:format=KotlinFqcn and optionally add ;kotlinx=SerializerFqcn for kotlinx.serialization. |
--dependencies-generation-mode |
Configure generation of file containing dependencies of the generated source code. Default: NONE |
| CHOOSE ONE OF: | |
NONE - No extra (dependencies) file is generated. |
|
GRADLE_NOTATION - Dependencies file is generated with gradle notation (expected to be placed inside dependencies {} block in build.gradle(.kts) |
|
MAVEN_NOTATION - Dependencies file is generated with maven notation (expected to be placed inside |
|
--external-ref-resolution |
Specify to which degree referenced schemas from external files are included in model generation. Default: TARGETED |
| CHOOSE ONE OF: | |
TARGETED - Generate referenced schemas and their polymorphic subtypes from external API files. |
|
AGGRESSIVE - Referencing any schema in an external API file triggers generation of every external schema in that file. |
|
--http-client-opts |
Select the options for the http client code that you want to be generated. |
| CHOOSE ANY OF: | |
SUSPEND_MODIFIER - This option adds the suspend modifier to the generated client functions (only for OpenFeign clients) |
|
SPRING_RESPONSE_ENTITY_WRAPPER - This option adds the Spring-ResponseEntity generic around the response to be able to get response headers and status (only for OpenFeign clients). |
|
SPRING_CLOUD_OPENFEIGN_STARTER_ANNOTATION - This option adds the @FeignClient annotation to generated client interface |
|
GROUP_BY_TAG - This option groups clients based on the first tag rather than paths |
|
OKHTTP_NON_NULL_RESPONSE_PAYLOADS - This option makes ApiResponse.data non-null. Responses declared with a body must return one: a missing body, or one that deserializes to null, throws ApiException. An operation that declares both a body response and an empty success response (e.g. 200 and 204) throws on the empty success. Binary responses return an empty ByteArray for an empty body (only for OkHttp clients) |
|
RESPONSE_MEDIA_TYPE_FUNCTIONS - This option adds, for each operation whose success responses declare a different JSON schema per media type, one function per media type. The function always sends that media type as the Accept header and returns that schema's type instead of JsonNode. Its name is the operation's function name plus the media subtype, e.g. getItemsVndCustomJson for application/vnd.custom+json (only for OkHttp, OpenFeign and Spring HTTP Interface clients) |
|
DYNAMIC_BASE_URL - This option makes ApiConfiguration.basePath empty, allowing you to set the base URL at runtime (only for Ktor clients) |
|
OPENAPI_API_KEY_AUTHENTICATION - Adds header API-key helpers and complete security alternatives with enabled Bearer and OAuth2 schemes |
|
OPENAPI_BEARER_AUTHENTICATION - Adds operation-specific Bearer token helpers for OpenAPI HTTP Bearer security schemes |
|
OPENAPI_OAUTH2_AUTHENTICATION - Adds operation-specific OAuth2 access-token helpers that pass required scopes to a caller-provided token provider |
|
--http-client-target |
Optionally select the target client that you want to be generated. Defaults to OK_HTTP |
| CHOOSE ONE OF: | |
OK_HTTP - Generate OkHttp client. |
|
OPEN_FEIGN - Generate OpenFeign client. |
|
SPRING_HTTP_INTERFACE - Generate Spring HTTP Interface. |
|
KTOR - Generate Ktor client. |
|
--http-controller-opts |
Select the options for the controllers that you want to be generated. |
| CHOOSE ANY OF: | |
SUSPEND_MODIFIER - This option adds the suspend modifier to the generated controller functions |
|
AUTHENTICATION - This option adds the authentication parameter to the generated controller functions |
|
GROUP_BY_TAG - This option groups controllers based on the first tag rather than paths |
|
COMPLETION_STAGE - This option makes generated controller functions have Type CompletionStage |
|
SSE_EMITTER - This option makes generated controller functions have Type SseEmitter (works only with Spring Controller generator) |
|
--http-controller-target |
Optionally select the target framework for the controllers that you want to be generated. Defaults to Spring Controllers |
| CHOOSE ONE OF: | |
SPRING - Generate for Spring framework. |
|
MICRONAUT - Generate for Micronaut framework. |
|
KTOR - Generate for Ktor server. |
|
--http-model-additional-annotations |
Repeatable fully qualified annotation class name to add to generated HTTP model types. Only annotations without arguments are supported. |
--http-model-opts |
Select the options for the http models that you want to be generated. |
| CHOOSE ANY OF: | |
EXCLUDE_READ_ONLY - Exclude readOnly properties and honour requiredness of retained writeOnly properties |
|
EXCLUDE_WRITE_ONLY - Exclude writeOnly properties and honour requiredness of retained readOnly properties |
|
REQUEST_RESPONSE_MODELS - Generate separate Request and Response model sets, excluding readOnly and writeOnly properties respectively |
|
SHARED_COMPOSITION_CONTRACTS - Expose explicitly included object schemas through shared Kotlin interfaces |
|
X_EXTENSIBLE_ENUMS - This option treats x-extensible-enums as enums |
|
JAVA_SERIALIZATION - This option adds Java Serializable interface to the generated models |
|
QUARKUS_REFLECTION - This option adds @RegisterForReflection to the generated models. Requires dependency "'io.quarkus:quarkus-core:+" |
|
MICRONAUT_INTROSPECTION - This option adds @Introspected to the generated models. Requires dependency "'io.micronaut:micronaut-core:+" |
|
MICRONAUT_REFLECTION - This option adds @ReflectiveAccess to the generated models. Requires dependency "'io.micronaut:micronaut-core:+" |
|
MICRONAUT_SERDEABLE - This option adds @Serdeable to the generated models. Requires dependency "'io.micronaut.serde:micronaut-serde-jackson:+" |
|
INCLUDE_COMPANION_OBJECT - This option adds a companion object to the generated models. |
|
NON_NULL_MAP_VALUES - This option makes map values non-null. The default (since v15) and most spec compliant is make map values nullable |
|
FAULT_TOLERANT_ENUMS - This option adds an UNRECOGNIZED enum entry as a fallback for unmapped values, preventing deserialization exceptions. If jackson is used, the deserialization option READ_UNKNOWN_ENUM_VALUES_USING_DEFAULT_VALUE will need to be enabled as well |
|
FAULT_TOLERANT_OPEN_ENUMS - This option converts the "open enum" pattern (an anyOf combining a string enum with an open type: string) into a fault-tolerant enum, i.e. an enum carrying the declared values plus an UNRECOGNIZED fallback, instead of collapsing the type to a plain String. Behaves like FAULT_TOLERANT_ENUMS for the affected enums |
|
--http-model-suffix |
Specify custom suffix for all generated model classes. Defaults to no suffix. |
--instant-library |
Specify which Instant library to use in generated model classes for kotlinx.serialization. Default: KOTLINX_INSTANT |
| CHOOSE ONE OF: | |
KOTLINX_INSTANT - Use kotlinx.datetime Instant in generated classes (default) |
|
KOTLIN_TIME_INSTANT - Use kotlin.time Instant in generated classes |
|
--jackson-nullability-mode |
Configure advanced handling when serializing null values with Jackson. Default: NONE |
| CHOOSE ONE OF: | |
NONE - Default Jackson behaviour |
|
ENFORCE_OPTIONAL_NON_NULL - Omit null values for optional non-null fields |
|
ENFORCE_REQUIRED_NULLABLE - Include null values for required nullable fields |
|
STRICT - Combines ENFORCE_OPTIONAL_NON_NULL and ENFORCE_REQUIRED_NULLABLE for strictest contract enforcement |
|
--json-schema-file |
Use a JSON Schema document (draft-04 through 2020-12) as the primary input instead of --api-file, converting it to an OpenAPI 3.1 document before generation. Accepts either a local file path or a resolvable http(s) URL, optionally with a trailing '#/json/pointer' fragment (RFC 6901) selecting the schema nested inside a larger resource such as a Nakadi EventType manifest — e.g. 'manifest.yaml#/spec/schemaObject'. An empty fragment ('manifest.yaml#') or no fragment treats the whole file as a bare JSON Schema document. Cannot be combined with --api-file. |
--json-schema-root-name |
Name for the schema generated from a JSON Schema's own top-level properties, used with --json-schema-file. Defaults to the schema's 'title', then the resource's '/metadata/name', then 'Schema' with a warning if neither is present. |
--openfeign-client-name |
Specify openfeign client name for spring-cloud-starter-openfeign. Defaults to 'fabrikt-client'. |
--operation-id-transform |
Regex replacement applied to every operationId before it becomes a generated function name, format ' |
--output-directory |
Allows the generation dir to be overridden. Defaults to current dir |
--output-opts |
Select options for the output. |
| CHOOSE ANY OF: | |
ADD_FILE_DISCLAIMER - This option adds a disclaimer to the generated files. |
|
ADD_GENERATED_ANNOTATION - Annotate generated types and top-level functions with javax.annotation.processing.Generated. |
|
--resources-path |
Allows the path for generated resources to be overridden. Defaults to src/main/resources |
--serialization-library |
Specify which serialization library to use for annotations in generated model classes. Default: JACKSON_3 |
| CHOOSE ONE OF: | |
JACKSON - Use Jackson 2 for serialization and deserialization |
|
JACKSON_2 - Use Jackson 2 for serialization and deserialization |
|
JACKSON_3 - Use Jackson 3 for serialization and deserialization |
|
KOTLINX_SERIALIZATION - Use kotlinx.serialization for serialization and deserialization |
|
--src-path |
Allows the path for generated source files to be overridden. Defaults to src/main/kotlin |
--targets |
Targets are the parts of the application that you want to be generated. |
| CHOOSE ANY OF: | |
HTTP_MODELS - Jackson annotated data classes to represent the schema objects defined in the input. |
|
CONTROLLERS - Spring / Micronaut / Ktor HTTP controllers for each of the endpoints defined in the input. |
|
CLIENT - Simple http rest client. |
|
QUARKUS_REFLECTION_CONFIG - This options generates the reflection-config.json file for quarkus integration projects |
|
--type-overrides |
Specify non-default kotlin types for certain OAS types. For example, generate Instant instead of OffsetDateTime |
| CHOOSE ANY OF: | |
DATETIME_AS_INSTANT - Use Instant as the datetime type. Defaults to OffsetDateTime |
|
DATETIME_AS_LOCALDATETIME - Use LocalDateTime as the datetime type. Defaults to OffsetDateTime |
|
DATETIME_AS_OFFSETDATETIME - Use OffsetDateTime as the datetime type. Default for Jackson |
|
BYTE_AS_STRING - Ignore string format byte and use String as the type |
|
BYTE_AS_BYTEARRAY - Use ByteArray as the byte type. Default |
|
BINARY_AS_STRING - Ignore string format binary and use String as the type |
|
BINARY_AS_BYTEARRAY - Use ByteArray as the binary type. Default |
|
URI_AS_STRING - Ignore string format uri and use String as the type |
|
URI_AS_URI - Use java.net.URI as the uri type. Default |
|
UUID_AS_STRING - Ignore string format uuid and use String as the type |
|
UUID_AS_UUID - Use java.util.UUID as the uuid type. Default |
|
DATE_AS_STRING - Ignore string format date and use String as the type |
|
DATE_AS_LOCALDATE - Use java.time.LocalDate as the date type. Default for Jackson |
|
DATE_AS_KOTLINXLOCALDATE - Use kotlinx.datetime.LocalDate as the date type. Default for kotlinx.serialization |
|
DATETIME_AS_STRING - Ignore string format date-time and use String as the type |
|
BYTEARRAY_AS_INPUTSTREAM - Use InputStream as ByteArray type. Defaults to ByteArray |
|
ANY_AS_JSONELEMENT - Use kotlinx.serialization.json.JsonElement for untyped (any) schemas and JsonObject for untyped objects. Requires the KOTLINX_SERIALIZATION serialization library. Defaults to Any |
|
ANY_AS_ANY - Use Any as the untyped (any) type. Default |
|
--validation-library |
Specify which validation library to use for annotations in generated model classes. Default: JAKARTA_VALIDATION |
| CHOOSE ONE OF: | |
JAVAX_VALIDATION - Use javax.validation annotations in generated model classes |
|
JAKARTA_VALIDATION - Use jakarta.validation annotations in generated model classes (default) |
|
NO_VALIDATION - Use no validation annotations in generated model classes |
Original Motivation
The team that built the first version of this tool initially contributed to the Kotlin code generation ability in OpenApiTools, but reached the limits of what could be achieved with template-based generation. This library leverages the rich OpenAPI 3 model provided by KaiZen-OpenApi-Parser and uses Kotlin Poet to programmatically construct Kotlin classes for maximum flexibility.
This project was started by engineers from Zalando Tech and is battle-tested heavily in production there.
Specific Features
Request and response models
By default, Fabrikt generates one model set and treats readOnly and writeOnly properties as optional. Directional model generation is opt-in:
--http-model-opts EXCLUDE_READ_ONLYexcludesreadOnlyproperties for a request-model generation pass.--http-model-opts EXCLUDE_WRITE_ONLYexcludeswriteOnlyproperties for a response-model generation pass.--http-model-opts REQUEST_RESPONSE_MODELSgenerates both model sets in one invocation, withRequestandResponseappended to model names.
In each directional model set, retained properties follow their declared requiredness and nullability. Filtering applies to properties of every type, including nested models and compositions. Selecting both exclusion options excludes both categories; explicit exclusion options also apply to both sets when combined with REQUEST_RESPONSE_MODELS.
For separate generation passes, use --http-model-suffix Request and --http-model-suffix Response to distinguish the model sets. With REQUEST_RESPONSE_MODELS, a custom suffix precedes the direction suffix, for example PetDtoRequest and PetDtoResponse. Both sets include enums and models without directional properties, keeping all generated references within their respective set. A model whose properties are all excluded becomes an empty Kotlin object.
When REQUEST_RESPONSE_MODELS is enabled, all supported clients and controllers use request models for incoming parameters and request bodies, and response models for returned bodies. Array and map element types follow the same direction. For targets that support multipart, requests omit excluded properties and use request models for structured parts. Existing multipart limitations remain unchanged: Micronaut controllers reject multipart parameters, while Ktor clients and controllers do not generate multipart part parameters. A type referenced by a parameter remains available even if its declaring property is excluded from a model. This option works with HTTP_MODELS, CLIENT, and CONTROLLERS; generating clients or controllers also generates both model sets.
Polymorphism via allOf
The following example shows how allOf can be used to generate polymorphic Kotlin data classes. It does the following:
- A
ChildDefinitionschema defines both the discriminator mapping details and the schema for the discriminator property. In this case the discriminator property is an enumeration - In each child schema,
allOfis used to merge theChildDefinitionwith the child's custom schema. This guarantees that each child schema inherits the correct discriminator property. - In the
Responsesschema aoneOflists only child schemas. This will be detected by Fabrikt and it will generate the list of parent types:List<ChildDefinition>
NOTE: Discriminated oneOf is the recommended way to model polymorphism. Fabrikt generates a Kotlin sealed interface for every oneOf, so no extra option is needed; the allOf pattern shown here remains supported for existing hierarchies.
openapi: 3.0.0
components:
schemas:
ChildDefinition:
type: object
discriminator:
propertyName: some_enum
mapping:
obj_one_only: '#/components/schemas/DiscriminatedChild1'
obj_two_first: '#/components/schemas/DiscriminatedChild2'
obj_two_second: '#/components/schemas/DiscriminatedChild2'
obj_three: '#/components/schemas/discriminated_child_3'
properties:
discriminating_property:
$ref: '#/components/schemas/ChildDiscriminator'
ChildDiscriminator:
type: string
enum:
- obj_one_only
- obj_two_first
- obj_two_second
- obj_three
DiscriminatedChild1:
allOf:
- $ref: '#/components/schemas/ChildDefinition'
- type: object
properties:
some_prop:
type: string
DiscriminatedChild2:
allOf:
- $ref: '#/components/schemas/ChildDefinition'
- type: object
properties:
some_prop:
type: string
discriminated_child_3:
allOf:
- $ref: '#/components/schemas/ChildDefinition'
Responses:
type: "object"
properties:
entries:
type: "array"
items:
oneOf:
- $ref: "#/components/schemas/DiscriminatedChild2"
- $ref: "#/components/schemas/DiscriminatedChild2"
- $ref: "#/components/schemas/discriminated_child_3"
@JsonTypeInfo(
use = JsonTypeInfo.Id.NAME,
include = JsonTypeInfo.As.EXISTING_PROPERTY,
property = "some_enum",
visible = true
)
@JsonSubTypes(
JsonSubTypes.Type(
value = DiscriminatedChild1::class,
name =
"obj_one_only"
),
JsonSubTypes.Type(
value = DiscriminatedChild2::class,
name =
"obj_two_first"
),
JsonSubTypes.Type(
value = DiscriminatedChild2::class,
name =
"obj_two_second"
),
JsonSubTypes.Type(value = DiscriminatedChild3::class, name = "obj_three")
)
sealed class ChildDefinition() {
abstract val someEnum: ChildDiscriminator
}
enum class ChildDiscriminator(
@JsonValue
val value: String
) {
OBJ_ONE_ONLY("obj_one_only"),
OBJ_TWO_FIRST("obj_two_first"),
OBJ_TWO_SECOND("obj_two_second"),
OBJ_THREE("obj_three");
companion object {
private val mapping: Map<String, ChildDiscriminator> =
values().associateBy(ChildDiscriminator::value)
fun fromValue(value: String): ChildDiscriminator? = mapping[value]
}
}
data class DiscriminatedChild1(
@param:JsonProperty("some_prop")
@get:JsonProperty("some_prop")
val someProp: String? = null,
@get:JsonProperty("some_enum")
@get:NotNull
override val someEnum: ChildDiscriminator = ChildDiscriminator.OBJ_ONE_ONLY
) : ChildDefinition()
data class DiscriminatedChild2(
@get:JsonProperty("some_enum")
@get:NotNull
override val someEnum: ChildDiscriminator,
@param:JsonProperty("some_prop")
@get:JsonProperty("some_prop")
val someProp: String? = null
) : ChildDefinition()
data class DiscriminatedChild3(
@get:JsonProperty("some_enum")
@get:NotNull
override val someEnum: ChildDiscriminator = ChildDiscriminator.OBJ_THREE
) : ChildDefinition()
data class Responses(
@param:JsonProperty("entries")
@get:JsonProperty("entries")
val entries: List<@Valid ChildDefinition>? = null
)
Bean validation
When a validated library is selected, @Valid is placed on the container's type argument for
collection properties and controller parameters, e.g. List<@Valid ChildDefinition> and
Map<String, @Valid ChildDefinition?>, rather than on the container itself
(List<ChildDefinition>). Hibernate Validator 9.1 deprecates the legacy container-level
placement.
Nested containers keep @Valid on the innermost element only (e.g.
List<List<@Valid ChildDefinition>>), and containers with a non-cascadable element such as
List<String> carry no @Valid.
Kotlin only writes type-argument annotations into bytecode when compiled with
-Xemit-jvm-type-annotations. Enable that flag in consumer projects so Hibernate Validator can see the cascading@Validat runtime.
Contributing
Building Locally
Fabrikt is built with Gradle and requires an initialised git repository. The easiest way to build it is to clone the repo locally before executing the build command:
git clone git@github.com:fabrikt-io/fabrikt.git
cd fabrikt/
./gradlew clean build
Adjusting Test Examples
A utility function is available in GeneratedCodeAsserter.kt to mass change all of the code generation examples in the test resources folder. This is useful when a global change is made to the code generation logic and all of the examples need to be updated.
Publishing
- Go to Release Tab
- Select
Draft a new release. - Set tag to a version greater than current using symantic versioning, anticipating whether the changes made could break builds.
- Click
Generate release notes. Ensure that the tag and release version match. - Click
Publish releasebuttom at the bottom.
Github Actions will publish the deployment to Sonatype Central. You must then log in to Sonatype and decide to either release or drop that deployment. After 30 minutes or so, Maven Central will have indexed the promoted release.
