cjbooms / fabrikt Goto Github PK
View Code? Open in Web Editor NEWGenerates Kotlin Code from OpenApi3 Specifications
License: Apache License 2.0
Generates Kotlin Code from OpenApi3 Specifications
License: Apache License 2.0
Inlined objects that are generated often share the same name as top level objects, or other inlined objects. This causes models to overwrite each other at generation time with unexpected results
openapi: 3.0.0
components:
schemas:
ContainsInLinedObject:
type: object
properties:
generation:
type: object
properties:
call_home:
type: object
required:
- url
properties:
url:
type: string
database_view:
type: object
required:
- view_name
properties:
view_name:
type: string
direct:
type: string
Should produce:
data class ContainsInLinedObject(
@param:JsonProperty("generation")
@get:JsonProperty("generation")
@get:Valid
val generation: ContainsInLinedObjectGeneration? = null
)
data class ContainsInLinedObjectGenerationCallHome(
@param:JsonProperty("url")
@get:JsonProperty("url")
@get:NotNull
val url: String
)
data class ContainsInLinedObjectGenerationDatabaseView(
@param:JsonProperty("view_name")
@get:JsonProperty("view_name")
@get:NotNull
val viewName: String
)
data class ContainsInLinedObjectGeneration(
@param:JsonProperty("call_home")
@get:JsonProperty("call_home")
@get:Valid
val callHome: ContainsInLinedObjectGenerationCallHome? = null,
@param:JsonProperty("database_view")
@get:JsonProperty("database_view")
@get:Valid
val databaseView: ContainsInLinedObjectGenerationDatabaseView? = null,
@param:JsonProperty("direct")
@get:JsonProperty("direct")
val direct: String? = null
)
Placeholder to incorporate a publish mechanism to be triggered by every merge to master
I noticed that the return type of clients is always nullable:
Is this simply a result of OkHttp's response.body
being nullable? For endpoints that return something, it might be more sensible to throw an ApiException
instead if the status code was 2xx but there was no body. Otherwise user code always needs a try-catch and null handling, which seems annoying.
Add support for generating Java Serializable models via an optional flag to the CLI for supporting this new feature:
---http-models-option 'java_serialisation'
Zalando's string format x-extensible-enum
could be generated as an enum if requested by users.
The default should be not to treat these strings as enums as it could cause clients to break as new enum values are added. But for server-side development, it may be nicer to generate these definitions as enums
Spring Controller method parameters need @DateTimeFormat annotations to correctly parse date / date-time values
For example
fun get(
@DateTimeFormat(iso = DateTimeFormat.ISO.DATE)
@RequestParam(value = "aDate", required = true)
aDate: LocalDate,
@DateTimeFormat(iso = DateTimeFormat.ISO.DATE_TIME)
@RequestParam(value = "bDateTime", required = true)
bDateTime: OffsetDateTime
): ResponseEntity<Unit>
This is described in https://www.baeldung.com/spring-date-parameters
Happy to contribute a PR to fix this.
The gradle task example in the readme does not play nice with Gradle's caching at all. The worst case scenario is that the cache key is not dependent on the input files, so even after changing the spec a stale cache entry will be used. This seems to be the case.
At a minimum, the example should be updated to use inputs.file("/path-to-api/open-api.yaml").withPathSensitivity(PathSensitivity.NONE)
, but ideally you could provide a small gradle plugin with a task type that sets up proper caching and provides setters/configuration methods for the most common CLI options.
It looks like polymorphic class generation breaks as soon as that class is referenced from another schema. Here's an example that adds a Wrapper
schema to the yml from the readme:
openapi: 3.0.0
components:
schemas:
+ Wrapper:
+ type: object
+ properties:
+ polymorph:
+ $ref: '#/components/schemas/PolymorphicEnumDiscriminator'
PolymorphicEnumDiscriminator:
type: object
discriminator:
propertyName: some_enum
mapping:
obj_one: '#/components/schemas/ConcreteImplOne'
obj_two: '#/components/schemas/ConcreteImplTwo'
properties:
some_enum:
$ref: '#/components/schemas/EnumDiscriminator'
ConcreteImplOne:
allOf:
- $ref: '#/components/schemas/PolymorphicEnumDiscriminator'
- type: object
properties:
some_prop:
type: string
ConcreteImplTwo:
allOf:
- $ref: '#/components/schemas/PolymorphicEnumDiscriminator'
- type: object
properties:
some_prop:
type: string
EnumDiscriminator:
type: string
enum:
- obj_one
- obj_two
In the generated code, PolymorphicEnumDiscriminator
is generated as a regular class
and the subtypes are missing:
data class Wrapper(
@param:JsonProperty("polymorph")
@get:JsonProperty("polymorph")
@get:Valid
val polymorph: PolymorphicEnumDiscriminator? = null
)
class PolymorphicEnumDiscriminator() {
@param:JsonProperty("some_enum")
@get:JsonProperty("some_enum")
@get:NotNull
val someEnum: EnumDiscriminator
}
enum class EnumDiscriminator(
@JsonValue
val value: String
) {
OBJ_ONE("obj_one"),
OBJ_TWO("obj_two");
companion object {
private val mapping: Map<String, EnumDiscriminator> =
values().associateBy(EnumDiscriminator::value)
fun fromValue(value: String): EnumDiscriminator? = mapping[value]
}
}
In openapi-generator, controller methods are named after the operationId
in the spec file. fabrikt just seems to name the methods after the HTTP method (post()
etc). This makes it impossible to implement multiple controllers with overlapping HTTP methods in the same class.
The simplest solution would be to use the operationId
like openapi-generator does - perhaps as an option as it would be breaking otherwise?
I could also see wanting to have this feature even apart from the mentioned multiple-controllers-in-one-class use case, simply because the operationId
, if present, is probably carries more information than the generated one.
This library looks great, are you planning to add support for kotlinx serialization, or would you accept a pr that adds it?
Below, the inline object in ConnectorResource has a model named Metadata created for it, overriding the one made for the top level Metadata schema. It's not clear how to handle this situation atm, possibly an error should be thrown or some naming strategy should be developed for the models created from inline objects, such as parent_field like we do in the sql.
Metadata:
type: object
properties:
labels:
type: string
annotations:
type: string
ConnectorResource:
type: object
required:
- metadata
properties:
metadata:
type: object
required:
- name
properties:
name:
type: string
data class Metadata(
@JsonProperty("name")
@get:NotNull
val name: String
)
The following operation:
/unsent-events:
get:
summary: "Page through all the UnsentEvent resources matching the query filters"
tags:
- "unsentevent"
parameters:
- $ref: "#/components/parameters/FlowId"
- $ref: "#/components/parameters/TeamIdsQueryParam"
- $ref: "#/components/parameters/AppIdsQueryParam"
- $ref: "#/components/parameters/IncludeInactive"
- $ref: "#/components/parameters/Limit"
- $ref: "#/components/parameters/Cursor"
- $ref: "#/components/parameters/TokenInfo"
responses:
200:
description: "successful operation"
headers:
Cache-Control:
$ref: "#/components/headers/CacheControl"
content:
application/json:
schema:
$ref: "#/components/schemas/UnsentEventQueryResult"
default:
description: "error occurred - see status code and problem object for more\
\ information."
content:
application/problem+json:
schema:
$ref: "https://zalando.github.io/problem/schema.yaml#/Problem"
security:
- oauth2:
- "uid"
- "fabric-event-scheduler.read"
is producing a Service interface with the following signature:
fun query(
limit: Int,
xTokeninfoForward: TokenInfo,
xFlowId: String?,
teamIds: List<String>?,
appIds: List<String>?,
includeInactive: Boolean?,
cursor: String?
): List<UnsentEvent>
But it should be producing this:
fun query(
limit: Int,
xTokeninfoForward: TokenInfo,
xFlowId: String?,
teamIds: List<String>?,
appIds: List<String>?,
includeInactive: Boolean?,
cursor: String?
): UnsentEventQueryResult
Using oneOf unions all fields, but with a discriminator it should create data classes with jackson subtypes. Currently oneOf + discriminator unions fields as usual and adds an empty subtypes list.
Generation:
type: object
description: |
This section configures how Fabric Event Scheduler will produce an event for a given table's row change.
oneOf:
- $ref: '#/components/schemas/CallHome'
- $ref: '#/components/schemas/DatabaseView'
- $ref: '#/components/schemas/DirectGeneration'
discriminator:
propertyName: generation_type
CallHome:
type: object
description: |
Use the Call-Home style to generate an event for a table row. In this style, when data is changed in a table row
Fabric Event Scheduler will call your endpoint with the name of the table and the value from the id column, configured
in the database section. The response will contain the event to be sent to Nakadi.
required:
- url
properties:
generation_type:
type: string
url:
type: string
description: |
Url to call to get the latest event state for a given table row.
DatabaseView:
type: object
description: |
Use the Database-View style to generate an event for a table row. In this style, when data is changed in a table row
Fabric Event Scheduler will take the value of this view as the event to be sent to Nakadi.
required:
- view_name
properties:
generation_type:
type: string
view_name:
type: string
description: |
Name of the view which will provide the latest state of a table row when queried.
DirectGeneration:
type: object
description: |
This section configures direct event generation. In this style, the nakadi event is produced by converting the updated
row to json.
properties:
generation_type:
type: string
@JsonTypeInfo(
use = JsonTypeInfo.Id.NAME,
include = JsonTypeInfo.As.EXISTING_PROPERTY,
property = "generation_type",
visible = true
)
@JsonSubTypes()
sealed class Generation(
open val generationType: String?,
open val url: String?,
open val viewName: String?
)
I have an OpenAPI specification that cannot be parsed for some reason. When fabrikt tries to convert it I get an error with the message Failed to resolve references when parsing API. External Schema references require internet connection
even though the specification contains no external references. My first hunch was that it is actually a relative path problem, but catching the exception that triggers this message (in YamlUtils.kt:41
) I am inclined to think the root NullPointerException
has nothing to do with the message and is actually a bug in the underlying library. That same specification can certainly be parsed with openapi-generator or SwaggerUI.
I can try to replicate the example as I cannot (yet) share the actual OpenAPI specification but the stacktrace of the triggering exception looks like this:
java.lang.NullPointerException
at com.reprezen.kaizen.oasparser.val.ValidatorBase.getAllowedJsonTypes(ValidatorBase.java:419)
at com.reprezen.kaizen.oasparser.val.ValidatorBase.validateField(ValidatorBase.java:222)
at com.reprezen.kaizen.oasparser.val.ValidatorBase.validateBooleanField(ValidatorBase.java:71)
at com.reprezen.kaizen.oasparser.val3.EncodingPropertyValidator.runObjectValidations(EncodingPropertyValidator.java:38)
at com.reprezen.kaizen.oasparser.val.ObjectValidatorBase.runValidations(ObjectValidatorBase.java:18)
at com.reprezen.kaizen.oasparser.val.ValidatorBase.validate(ValidatorBase.java:65)
at com.reprezen.kaizen.oasparser.val.MapValidator.runValidations(MapValidator.java:31)
at com.reprezen.kaizen.oasparser.val.ValidatorBase.validate(ValidatorBase.java:65)
at com.reprezen.kaizen.oasparser.val.ValidatorBase.validateMap(ValidatorBase.java:285)
at com.reprezen.kaizen.oasparser.val.ValidatorBase.validateMapField(ValidatorBase.java:279)
at com.reprezen.kaizen.oasparser.val3.MediaTypeValidator.runObjectValidations(MediaTypeValidator.java:38)
at com.reprezen.kaizen.oasparser.val.ObjectValidatorBase.runValidations(ObjectValidatorBase.java:18)
at com.reprezen.kaizen.oasparser.val.ValidatorBase.validate(ValidatorBase.java:65)
at com.reprezen.kaizen.oasparser.val.MapValidator.runValidations(MapValidator.java:31)
at com.reprezen.kaizen.oasparser.val.ValidatorBase.validate(ValidatorBase.java:65)
at com.reprezen.kaizen.oasparser.val.ValidatorBase.validateMap(ValidatorBase.java:285)
at com.reprezen.kaizen.oasparser.val.ValidatorBase.validateMapField(ValidatorBase.java:279)
at com.reprezen.kaizen.oasparser.val3.RequestBodyValidator.runObjectValidations(RequestBodyValidator.java:28)
at com.reprezen.kaizen.oasparser.val.ObjectValidatorBase.runValidations(ObjectValidatorBase.java:18)
at com.reprezen.kaizen.oasparser.val.ValidatorBase.validate(ValidatorBase.java:65)
at com.reprezen.kaizen.oasparser.val.ValidatorBase.validateField(ValidatorBase.java:225)
at com.reprezen.kaizen.oasparser.val3.OperationValidator.runObjectValidations(OperationValidator.java:48)
at com.reprezen.kaizen.oasparser.val.ObjectValidatorBase.runValidations(ObjectValidatorBase.java:18)
at com.reprezen.kaizen.oasparser.val.ValidatorBase.validate(ValidatorBase.java:65)
at com.reprezen.kaizen.oasparser.val.MapValidator.runValidations(MapValidator.java:31)
at com.reprezen.kaizen.oasparser.val.ValidatorBase.validate(ValidatorBase.java:65)
at com.reprezen.kaizen.oasparser.val.ValidatorBase.validateMap(ValidatorBase.java:285)
at com.reprezen.kaizen.oasparser.val.ValidatorBase.validateMapField(ValidatorBase.java:279)
at com.reprezen.kaizen.oasparser.val3.PathValidator.runObjectValidations(PathValidator.java:32)
at com.reprezen.kaizen.oasparser.val.ObjectValidatorBase.runValidations(ObjectValidatorBase.java:18)
at com.reprezen.kaizen.oasparser.val.ValidatorBase.validate(ValidatorBase.java:65)
at com.reprezen.kaizen.oasparser.val.MapValidator.runValidations(MapValidator.java:31)
at com.reprezen.kaizen.oasparser.val.ValidatorBase.validate(ValidatorBase.java:65)
at com.reprezen.kaizen.oasparser.val.ValidatorBase.validateMap(ValidatorBase.java:285)
at com.reprezen.kaizen.oasparser.val.ValidatorBase.validateMapField(ValidatorBase.java:279)
at com.reprezen.kaizen.oasparser.val3.OpenApi3Validator.runObjectValidations(OpenApi3Validator.java:56)
at com.reprezen.kaizen.oasparser.val.ObjectValidatorBase.runValidations(ObjectValidatorBase.java:18)
at com.reprezen.kaizen.oasparser.val.ValidatorBase.validate(ValidatorBase.java:65)
at com.reprezen.kaizen.oasparser.ovl3.OpenApi3Impl.validate(OpenApi3Impl.java:60)
at com.reprezen.kaizen.oasparser.OpenApiParser.parse(OpenApiParser.java:96)
at com.reprezen.kaizen.oasparser.OpenApiParser.parse(OpenApiParser.java:85)
at com.reprezen.kaizen.oasparser.OpenApiParser.parse(OpenApiParser.java:35)
at com.reprezen.kaizen.oasparser.OpenApi3Parser.parse(OpenApi3Parser.java:29)
at com.reprezen.kaizen.oasparser.OpenApi3Parser.parse(OpenApi3Parser.java:20)
at com.reprezen.kaizen.oasparser.OpenApiParser.parse(OpenApiParser.java:28)
at com.reprezen.kaizen.oasparser.OpenApi3Parser.parse(OpenApi3Parser.java:24)
at com.cjbooms.fabrikt.util.YamlUtils.parseOpenApi(YamlUtils.kt:39)
at com.cjbooms.fabrikt.model.SourceApi.<init>(SourceApi.kt:32)
at com.cjbooms.fabrikt.model.SourceApi$Companion.create(SourceApi.kt:28)
at com.cjbooms.fabrikt.cli.CodeGen.generate(CodeGen.kt:50)
at com.cjbooms.fabrikt.cli.CodeGen.main(CodeGen.kt:22)
If you have an additionalProperties
schema described as per below, there is a stack trace thrown during model generation.
additionalProperties:
oneOf:
- $ref: '#/component/schema/1'
- $ref: '#/component/schema/2'
Exception in thread "main" java.lang.IllegalStateException: Unknown OAS type: object and format: null
at ie.zalando.fabric.model.OasType$Companion.toOasType(OasType.kt:45)
at ie.zalando.fabric.model.PolyglotTypeInfo$Companion.from(PolyglotTypeInfo.kt:60)
at ie.zalando.fabric.model.PolyglotTypeInfo$Companion.from(PolyglotTypeInfo.kt:77)
at ie.zalando.fabric.model.PolyglotTypeInfo$Companion.from(PolyglotTypeInfo.kt:77)
at ie.zalando.fabric.model.PropertyInfo$MapField.<init>(PropertyInfo.kt:206)
at ie.zalando.fabric.model.PropertyInfo$Companion.getInLinedProperties(PropertyInfo.kt:89)
at ie.zalando.fabric.model.PropertyInfo$Companion.topLevelProperties(PropertyInfo.kt:68)
at ie.zalando.fabric.model.PropertyInfo$Companion.topLevelProperties(PropertyInfo.kt:59)
at ie.zalando.fabric.model.ModelInfo.<init>(ModelInfo.kt:57)
at ie.zalando.fabric.model.ModelInfo.<init>(ModelInfo.kt:32)
at ie.zalando.fabric.model.ModelInfo$Companion.modelInfosFromApi$fabric_generation_station(ModelInfo.kt:109)
at ie.zalando.fabric.model.ApiSeed.<init>(ApiSeed.kt:39)
at ie.zalando.fabric.model.ApiSeed$Companion.create(ApiSeed.kt:24)
at ie.zalando.fabric.cli.CodeGen.generate(CodeGen.kt:41)
at ie.zalando.fabric.cli.CodeGen.main(CodeGen.kt:22)
Generated class names are inconsistent between the variable data type and its related class. This only happens when the inline object subtype is an array. The generated class name is prefixed correctly, but the variable data type of the parent class is not.
openapi: 3.0.0
components:
schemas:
EnrichmentValue:
type: object
required:
- versioned
- owner_id
properties:
versioned:
type: array
items:
type: object
properties:
version:
type: string
should generate:
data class EnrichmentValue(
@param:JsonProperty("versioned")
@get:JsonProperty("versioned")
@get:NotNull
@get:Valid
val versioned: List<EnrichmentValueVersioned>
) : Serializable
but it's generating instead:
data class EnrichmentValue(
@param:JsonProperty("versioned")
@get:JsonProperty("versioned")
@get:NotNull
@get:Valid
val versioned: List<Versioned>
) : Serializable
It would be hugely beneficial if Fabrikt could generate from Json Schema as well as OpenApi3.
There seems to be an existing NPM package that might make this easy:
https://github.com/openapi-contrib/json-schema-to-openapi-schema
The accept header is wrongly hard-coded:
operation.getBodyResponses().firstOrNull()?.let {
this.add("\n.%T(%S, %S)", "header".toClassName(packages.client), "Accept", "application/json")
}
In Micronaut 3, the @introspected annotation is no longer used for adding a class to the reflect-config.json
file, which has been replaced by the @ReflectiveAccess annotation. Please, add a new cli option to support this new annotation like micronaut_reflection
for the generated models
INTROSPECTIONS AND GRAALVM REFLECTION
In previous versions of the Micronaut framework, adding @introspected to a class also added the configuration for GraalVM to allow for reflective usage of the class. This was the right choice to make prior to advancements made within the Framework, specifically in regards to validation and JSON encoding/decoding. The vast majority of cases should not require any reflection for introspected classes, and thus reflective metadata for GraalVM is no longer applied automatically.
To restore this behavior for an individual class, add the @ReflectiveAccess annotation to the class.
Currently we only generate models from the schemas and parameters sections
Consider expanding generation to support generating models from definitions within requestBodies also
This could be as simple as this:
SchemaInfo( ... ) {
...
allSchemas = openApi3.schemas.entries.map { it.key to it.value }
.plus(openApi3.parameters.entries.map { it.key to it.value.schema })
.plus(
openApi3.requestBodies.entries.flatMap { requestBody ->
requestBody.value.contentMediaTypes.map { requestBody.key to it.value.schema }
}
)
.map { (key, schema) -> SchemaInfo(key, schema) }
But lots test cases are need to validate this
Add support for the code generation of Spring Controllers
While this was definitely a copy-and-paste mistake in my OAI spec, I was still kinda surprised to see that fabrikt happily generates this.
Automate the versioning by taking the project.version
from the latest github release
Please add micronaut @Introspected
annotation to the model classes in order to ease the integration with the Micronaut framework: https://docs.micronaut.io/1.2.6/guide/index.html#_making_a_bean_available_for_introspection
Perhaps a cli option similar to the one we use for quarkus-reflection
?
Code generation for the following example is correct (return types correctly typed as Result
):
openapi: 3.0.0
paths:
/test:
get:
operationId: test
responses:
'200':
description: Operation successful
content:
application/json:
schema:
$ref: '#/components/schemas/Result'
components:
schemas:
Result:
allOf:
- $ref: '#/components/schemas/Base'
- type: object
properties:
bar:
type: string
Base:
properties:
foo:
type: string
However, when Result
is reduced to just this, the return type in both the client and the controller is suddenly Any
:
Result:
allOf:
- $ref: '#/components/schemas/Base'
I think an allOf
with a single item should be treated the same as a direct $ref
.
The exception classes generated by Fabrikt for the client are not triggering Spring's @Transactional to rollback transactions.
This is because the Fabrikt generated exceptions extend Exception instead of RuntimeException.
In its default configuration, the Spring Frameworkโs transaction infrastructure code only marks a transaction for rollback in the case of runtime, unchecked exceptions; that is, when the thrown exception is an instance or subclass of RuntimeException.
It should be possible to generate usage instuctions in mardown via a Gradle task that looks like:
val printCodeGenUsage by creating(JavaExec::class) {
dependsOn(fatJar)
classpath = project.files("./build/libs/$executableName.jar")
main = "ie.zalando.fabric.cli.CodeGen"
args = listOf("--help")
}
I have this spec (given below). When running openapi-generator on it, I get 3 classes: Foo
, Bar
and Baz
. Fabrikt only gives me Bar
. I've derived this from a bigger example and the issue seems to be that only models included via #/components/schemas
are processed.
openapi: 3.0.3
info:
title: title
description: description
version: 1.0.0
paths:
/foo:
get:
responses:
"200":
description: ok
content:
application/json:
schema:
type: object
title: Foo
properties:
fifo:
type: string
required:
- fifo
/bar:
get:
responses:
"200":
description: ok
content:
application/json:
schema:
$ref: '#/components/schemas/Bar'
/baz:
get:
responses:
"200":
description: ok
content:
application/json:
schema:
$ref: ./Baz.yaml
components:
schemas:
Bar:
type: object
properties:
baz:
type: string
Baz.yaml, for completness sake:
properties:
baz:
type: string
Currently, Fabrikt only supports the use of oneOf
on object
types. By OAS 3.0, it is allowed to use pretty much any type as well, like so:
oneOf:
- type: integer
At the moment, this causes an error:
Exception in thread "main" java.lang.IllegalStateException: Unknown OAS type: integer and format: null and specialization: ONE_OF_ANY
at com.cjbooms.fabrikt.model.OasType$Companion.toOasType(OasType.kt:62)
at com.cjbooms.fabrikt.model.KotlinTypeInfo$Companion.from(KotlinTypeInfo.kt:55)
at com.cjbooms.fabrikt.model.PropertyInfo$Field.<init>(PropertyInfo.kt:155)
at com.cjbooms.fabrikt.model.PropertyInfo$Companion.getInLinedProperties(PropertyInfo.kt:114)
at com.cjbooms.fabrikt.model.PropertyInfo$Companion.topLevelProperties(PropertyInfo.kt:54)
at com.cjbooms.fabrikt.generators.model.JacksonModelGenerator.createModels(JacksonModelGenerator.kt:152)
at com.cjbooms.fabrikt.generators.model.JacksonModelGenerator.generate(JacksonModelGenerator.kt:134)
at com.cjbooms.fabrikt.cli.CodeGenerator.models(CodeGenerator.kt:58)
at com.cjbooms.fabrikt.cli.CodeGenerator.generateControllerInterfaces(CodeGenerator.kt:44)
at com.cjbooms.fabrikt.cli.CodeGenerator.generateCode(CodeGenerator.kt:36)
at com.cjbooms.fabrikt.cli.CodeGenerator.generate(CodeGenerator.kt:31)
at com.cjbooms.fabrikt.cli.CodeGen.generate(CodeGen.kt:58)
at com.cjbooms.fabrikt.cli.CodeGen.main(CodeGen.kt:22)
Not all fields that should be non-null are in a spec like this for object B:
components:
schemas:
A:
type: object
required:
- a
properties:
a:
type: string
b:
type: string
B:
allOf:
- $ref: '#/components/schemas/A'
- type: object
required:
- b
I'd expect code something like this:
data class A(val a: String, val b: String?)
data class B(val a: String, val b: String)
Instead I get:
data class A(val a: String, val b: String?)
data class B(val a: String, val b: String?)
The most recent OpenAPI standard is version 3.1. Fabrikt currently uses KaiZen-OpenApi-Parser, which only supports 3.0 and is unlikely to be updated further, since the last commit on that project happened three years ago.
It would be nice if Fabrikt could support OpenAPI 3.1 as well, using something like swagger-parser instead.
This is a followup for #106.
openapi-generator considers foo
to be of type "any" in the following example using oneOf
:
openapi: 3.0.0
components:
schemas:
Foo:
type: object
properties:
foo:
oneOf:
- type: string
- type: object
properties:
a:
type: string
b:
type: string
It looks like right now fabrikt just uses the first schema here:
data class Foo(
@param:JsonProperty("foo")
@get:JsonProperty("foo")
val foo: String? = null
)
openapi-generator and swagger-codegen support "any" types via typeless schemas:
https://swagger.io/docs/specification/data-models/data-types/#any
It seems right now fabrikt doesn't support this:
openapi: 3.0.0
components:
schemas:
Foo:
type: object
properties:
bar:
$ref: '#/components/schemas/AnyValue'
AnyValue:
description: Can be any value - string, number, boolean, array or object.
Exception in thread "main" com.beust.jcommander.ParameterException: Invalid models or api file:
ValidationError(reason=Property 'bar' cannot be parsed to a Schema. Check your input)
at com.cjbooms.fabrikt.model.SourceApi.<init>(SourceApi.kt:37)
at com.cjbooms.fabrikt.model.SourceApi$Companion.create(SourceApi.kt:28)
at com.cjbooms.fabrikt.cli.CodeGen.generate(CodeGen.kt:50)
at com.cjbooms.fabrikt.cli.CodeGen.main(CodeGen.kt:22)
By default openapi-generator uses kotlin.Any
: OpenAPITools/openapi-generator#10070
In our project we map AnyType
to Jackson's JsonNode
since that seemed a bit more convenient to work with.
It should be possible to generate nested polymorphic models. A schema could be both, SuperType and SubType, however only one is actual resolving
Example:
Given the API definition below
schemas:
RootDiscriminatorType:
type: string
enum:
- firstLevelChild
FirstLevelDiscriminatorType:
type: string
enum:
- secondLevelChild1
- secondLevelChild2
RootType:
type: object
required:
- rootDiscriminator
properties:
rootDiscriminator:
$ref: "#/components/schemas/RootDiscriminatorType"
rootField1:
type: string
rootField2:
type: boolean
discriminator:
propertyName: rootDiscriminator
mapping:
firstLevelChild: '#/components/schemas/FirstLevelChild'
FirstLevelChild:
allOf:
- $ref: '#/components/schemas/RootType'
- type: object
required:
- firstLevelDiscriminator
properties:
firstLevelDiscriminator:
$ref: '#/components/schemas/FirstLevelDiscriminatorType'
firstLevelField1:
type: string
firstLevelField2:
type: integer
discriminator:
propertyName: firstLevelDiscriminator
mapping:
secondLevelChild1: '#/components/schemas/SecondLevelChild1'
SecondLevelChild1:
allOf:
- $ref: '#/components/schemas/FirstLevelChild'
- type: object
required:
- metadata
properties:
metadata:
$ref: '#/components/schemas/SecondLevelMetadata'
I would expect the model hierarchy to be
RootType->FirstLevelChild -> SecondLevelChild
Actual generation
RootType -> FirstLevelChild
SecondLevelChild
spring-webflux supports Kotlin suspend
methods in controllers. Seems like this would be simple for fabrikt to support, though it would have to be optional.
The suspend
modifier should be the only change that's needed for webflux support, otherwise the controllers generated by fabrikt look pretty much identical to the ones we use right now generated by openapi-generator.
So given two files:
api.yaml:
openapi: 3.0.0
paths: {}
info:
title: ""
version: ""
components:
schemas:
Wrapper:
type: object
properties:
polymorph:
$ref: 'external-models.yaml#/components/schemas/PolymorphicEnumDiscriminator'
and
external-models.yaml:
openapi: 3.0.0
paths: {}
info:
title: ""
version: ""
components:
schemas:
PolymorphicEnumDiscriminator:
type: object
discriminator:
propertyName: some_enum
mapping:
obj_one_only: '#/components/schemas/ConcreteImplOne'
obj_two_first: '#/components/schemas/ConcreteImplTwo'
obj_two_second: '#/components/schemas/ConcreteImplTwo'
obj_three: '#/components/schemas/ConcreteImplThree'
properties:
some_enum:
$ref: '#/components/schemas/EnumDiscriminator'
ConcreteImplOne:
allOf:
- $ref: '#/components/schemas/PolymorphicEnumDiscriminator'
- type: object
properties:
some_prop:
type: string
ConcreteImplTwo:
allOf:
- $ref: '#/components/schemas/PolymorphicEnumDiscriminator'
- type: object
properties:
some_prop:
type: string
ConcreteImplThree:
allOf:
- $ref: '#/components/schemas/PolymorphicEnumDiscriminator'
EnumDiscriminator:
type: string
enum:
- obj_one_only
- obj_two_first
- obj_two_second
- obj_three
Generated source file PolymorphicEnumDiscriminator.kt is:
package com.example.models
import com.fasterxml.jackson.annotation.JsonSubTypes
import com.fasterxml.jackson.annotation.JsonTypeInfo
@JsonTypeInfo(
use = JsonTypeInfo.Id.NAME,
include = JsonTypeInfo.As.EXISTING_PROPERTY,
property = "some_enum",
visible = true
)
@JsonSubTypes()
sealed class PolymorphicEnumDiscriminator() {
abstract val someEnum: EnumDiscriminator
}
I would expect it to work just as it does if the schemas are defined in the api.yaml
file.
If the servers.url
section is provided in the spec, the full path should be appended to the baseUrl
for every route. It should just use the root "/" otherwise.
e.g.
Spec:
servers:
- url: https://myapp.mydomain.com/path1/path2
...
paths:
/auth:
should generate on the client:
"$baseUrl/path1/path2/auth"
If your schema casing isn't consistent with the class name casing, the JsonSubtypes value of polymorphic discriminator objects is empty.
e.g., given this (where only the case of polymorphicEnumDiscriminator
is modified from the example in the project:
openapi: 3.0.0
paths: {}
info:
title: ""
version: ""
components:
schemas:
Wrapper:
type: object
properties:
polymorph:
$ref: '#/components/schemas/polymorphicEnumDiscriminator'
polymorphicEnumDiscriminator:
type: object
discriminator:
propertyName: some_enum
mapping:
obj_one_only: '#/components/schemas/ConcreteImplOne'
obj_two_first: '#/components/schemas/ConcreteImplTwo'
obj_two_second: '#/components/schemas/ConcreteImplTwo'
obj_three: '#/components/schemas/ConcreteImplThree'
properties:
some_enum:
$ref: '#/components/schemas/EnumDiscriminator'
ConcreteImplOne:
allOf:
- $ref: '#/components/schemas/polymorphicEnumDiscriminator'
- type: object
properties:
some_prop:
type: string
ConcreteImplTwo:
allOf:
- $ref: '#/components/schemas/polymorphicEnumDiscriminator'
- type: object
properties:
some_prop:
type: string
ConcreteImplThree:
allOf:
- $ref: '#/components/schemas/polymorphicEnumDiscriminator'
EnumDiscriminator:
type: string
enum:
- obj_one_only
- obj_two_first
- obj_two_second
- obj_three
you get:
@JsonTypeInfo(
use = JsonTypeInfo.Id.NAME,
include = JsonTypeInfo.As.EXISTING_PROPERTY,
property = "some_enum",
visible = true
)
@JsonSubTypes()
sealed class PolymorphicEnumDiscriminator() {
abstract val someEnum: EnumDiscriminator
}
It doesn't seem like the case of anything else matters, just the discriminator object.
For this API operation:
/refeed:
post:
summary: "Schedule a list of events which are to be resent to Nakadi in their current state"
parameters:
- $ref: "#/components/parameters/FlowId"
- $ref: "#/components/parameters/TokenInfo"
requestBody:
$ref: "#/components/requestBodies/RefeedBody"
responses:
202:
description: "The events were successfully rescheduled"
default:
description: "error occurred - see status code and problem object for more\
\ information."
content:
application/problem+json:
schema:
$ref: "https://zalando.github.io/problem/schema.yaml#/Problem"
security:
- oauth2:
- "uid"
The Service Interface generated for a POST with 202 response body has the following signature:
fun create(
refeedBody: RefeedBody,
xTokeninfoForward: TokenInfo,
xFlowId: String?
): Pair<URI, RefeedBody?>
}
This is fine for a 201 response, as the Location header is mandatory according to the RFC and is used in the generated Controller code.
For 202 and other responses, the URI should probably be nullable in the service interface signature:
fun create(
refeedBody: RefeedBody,
xTokeninfoForward: TokenInfo,
xFlowId: String?
): Pair<URI?, RefeedBody?>
}
Or possibly even be changed to:
fun create(
refeedBody: RefeedBody,
xTokeninfoForward: TokenInfo,
xFlowId: String?
): Unit
}
Otherwise service implementations are forced to create a dummy URI that is discarded in the generated controller code
The support for external referenced schemas is incomplete. When a schema in an external API spec makes use of aggregators anyOf
oneOf
or allOf
fabrikt fails to find the additional schemas.
For example in this test scenario, if we modify the external API spec to include the following oneOf and allOf combinations:
Primary API spec
openapi: 3.0.0
components:
schemas:
ContainingExternalReference:
type: object
properties:
some-external-reference:
$ref: './external-models.yaml#/components/schemas/ExternalObject'
Externally Referenced Spec
openapi: 3.0.0
components:
schemas:
ExternalObject:
type: object
properties:
another:
$ref: "#/components/schemas/ExternalObjectTwo"
one_of:
$ref: "#/components/schemas/ExternalOneOf"
ExternalObjectTwo:
type: object
required:
- errors
properties:
list-others:
type: array
items:
$ref: '#/components/schemas/ExternalObjectThree'
ExternalObjectThree:
type: object
required:
- enum
- description
properties:
enum:
type: string
enum:
- one
- two
- three
description:
type: string
ExternalOneOf:
oneOf:
- $ref: '#/components/schemas/OneOfOne'
- $ref: '#/components/schemas/OneOfTwo'
ParentOneOf:
type: object
discriminator:
propertyName: discriminator
properties:
discriminator:
type: string
OneOfOne:
allOf:
- $ref: '#/components/schemas/ParentOneOf'
- type: object
properties:
oneOfOne:
type: string
OneOfTwo:
allOf:
- $ref: '#/components/schemas/ParentOneOf'
- type: object
properties:
oneOfTwo:
type: string
Then the generated models do not contain data classes for
ExternalOneOf
ParentOneOf
OneOfOne
or OneOfTwo
If the same field name is used in separate objects for their individual discriminators then generation station is adding all possible classes as JSON deserialization sub classes.
Please see below example:
openapi: 3.0.0
components:
schemas:
PolymorphicSuperTypeOne:
x-fabric-resource-definition: true
type: object
discriminator:
propertyName: shared
required:
- shared
properties:
shared:
type: string
PolymorphicTypeOneA:
allOf:
- $ref: "#/components/schemas/PolymorphicSuperTypeOne"
- type: object
properties:
whateverA:
type: string
PolymorphicTypeOneB:
allOf:
- $ref: "#/components/schemas/PolymorphicSuperTypeOne"
- type: object
properties:
whateverB:
type: integer
format: int32
PolymorphicSuperTypeTwo:
x-fabric-resource-definition: true
type: object
discriminator:
propertyName: shared
required:
- shared
properties:
shared:
type: string
PolymorphicTypeTwoA:
allOf:
- $ref: "#/components/schemas/PolymorphicSuperTypeTwo"
- type: object
properties:
whateverC:
type: string
PolymorphicTypeTwoB:
allOf:
- $ref: "#/components/schemas/PolymorphicSuperTypeTwo"
- type: object
properties:
whateverD:
type: integer
format: int32
Generates something like:
@JsonTypeInfo(
use = JsonTypeInfo.Id.NAME,
include = JsonTypeInfo.As.EXISTING_PROPERTY,
property = "shared",
visible = true
)
@JsonSubTypes(
JsonSubTypes.Type(
value = PolymorphicTypeOneA::class,
name =
"PolymorphicTypeOneA"
),
JsonSubTypes.Type(
value = PolymorphicTypeOneB::class,
name =
"PolymorphicTypeOneB"
),
JsonSubTypes.Type(
value = PolymorphicTypeTwoA::class,
name =
"PolymorphicTypeTwoA"
),
JsonSubTypes.Type(
value = PolymorphicTypeTwoB::class,
name =
"PolymorphicTypeTwoB"
)
)
sealed class PolymorphicSuperTypeOne(
open val shared: String
)
Add a cli
option for generating the Spring service implementations with empty body templates. Something like:
@Service
class MyResourceServiceImpl() : MyResourceService {
override fun read(param1: String, param2: String?): Customer {
TODO("Not Implemented Yet!")
}
}
Follow-up for #111: for the sake of consistency, it seems sensible to use the operationId
in clients as well, not just controllers. It looks like right now, the name is generated based on the path (/example-path-1
-> getExamplePath1
).
I tried downloading the latest release from the Releases tab. When I tried building it, I got the following error:
* Where:
Build file '/home/gregor/src/fabrikt-2.1.1/build.gradle.kts' line: 4
* What went wrong:
An exception occurred applying plugin request [id: 'com.palantir.git-version', version: '0.12.3']
> Failed to apply plugin 'com.palantir.git-version'.
> Cannot find '.git' directory
Running git init .
helped and I could then build the project.
Code generation of Controllers would be greatly simplified if we focused on generating interfaces rather than implementations. We would no longer need to generate service interfaces
Client generation is using the following as input to Jackson methods:
Map<String, Any>::class.java
This is not valid when using parameterised types. It should be migrated over to use Jackson's TypeReference object:
typeRef: TypeReference<T>
A string with format type UUID should generate a data class using UUID instead of String.
properties:
stock_location_id:
description: >
The ID of the Stock Location from which this reservation should be fulfilled.
type: string
format: uuid
Should result in:
val stockLocationId: UUID
In our codebase, we have a lot of single-argument data classes to wrap raw strings / integers / UUIDs / etc. This improves type safety (mixing up different things that happen to have the same type now causes a compiler error) and readability. With openapi-generator, we were able to get the generated models to use these (hand-written) wrappers by using their type and import mapping features.
However, I'm thinking that ideally, the wrappers would be generated as well. Basically, the idea would be that if there is an explicit named schema, it is always generated. Right now, the following simply "inlines" the AccountToken
schema, so Account
has a token: UUID
argument:
openapi: 3.0.0
components:
schemas:
Account:
type: object
required:
- token
properties:
token:
$ref: '#/components/schemas/AccountToken'
AccountToken:
type: string
format: uuid
With a potential new CLI flag, it could instead generate something like this instead:
data class Account(
@param:JsonProperty("token")
@get:JsonProperty("token")
@get:NotNull
val token: AccountToken
)
data class AccountToken(
@get:JsonValue
@get:NotNull
val value: UUID
)
The @get:JsonValue
ensures that Jackson (de)serialization works as expected, i.e. treating it as a JSON string rather than a JSON object. In the future, it might even make sense for the wrappers to be @JvmInline
value classes, right now it seems Jackson doesn't like those.
The other complication here (and technically a separate problem) is that we have a lot of these wrapper schemas in shared spec files (think $ref: 'shared/tokens.yml#/components/schemas/AccountToken'
) used by many different APIs, and we generate each API into a separate Gradle module. By default that would lead to a lot of duplicated types being generated that are not compatible with each other. To avoid that we would need some mechanism to say "if the schema comes from a separate yml file, don't generate a type and instead assume it can be found in this package" (so perhaps a map from .yml
file path to package paths).
These two things seem to be the biggest remaining blockers for us to be able to switch from openapi-generator to fabrikt.
Would you be interested in supporting these use cases in fabrikt? If desired I could have a stab at implementing this in the form of a pull request sometime. I'm open to discussing alternative ideas as well. :)
Add a cli option that will generate the reflection-config.json
out of the schema definitions. Such like this:
[
{
"name" : "package.GeneratedModel",
"allDeclaredConstructors" : true,
"allPublicConstructors" : true,
"allDeclaredMethods" : true,
"allPublicMethods" : true,
"allDeclaredFields" : true,
"allPublicFields" : true
},
...
A declarative, efficient, and flexible JavaScript library for building user interfaces.
๐ Vue.js is a progressive, incrementally-adoptable JavaScript framework for building UI on the web.
TypeScript is a superset of JavaScript that compiles to clean JavaScript output.
An Open Source Machine Learning Framework for Everyone
The Web framework for perfectionists with deadlines.
A PHP framework for web artisans
Bring data to life with SVG, Canvas and HTML. ๐๐๐
JavaScript (JS) is a lightweight interpreted programming language with first-class functions.
Some thing interesting about web. New door for the world.
A server is a program made to process requests and deliver data to clients.
Machine learning is a way of modeling and interpreting data that allows a piece of software to respond intelligently.
Some thing interesting about visualization, use data art
Some thing interesting about game, make everyone happy.
We are working to build community through open source technology. NB: members must have two-factor auth.
Open source projects and samples from Microsoft.
Google โค๏ธ Open Source for everyone.
Alibaba Open Source for everyone
Data-Driven Documents codes.
China tencent open source team.