Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 6 additions & 0 deletions .github/workflows/samples-kotlin-server-jdk17.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ name: Samples Kotlin server (jdk17)
on:
push:
paths:
- 'samples/server/others/kotlin-server/**'
- 'samples/server/petstore/kotlin-springboot-3*/**'
- 'samples/server/petstore/kotlin-server/**'
- 'samples/server/petstore/kotlin-server-modelMutable/**'
Expand All @@ -13,6 +14,7 @@ on:
# - samples/server/petstore/kotlin-spring-default/**
pull_request:
paths:
- 'samples/server/others/kotlin-server/**'
- 'samples/server/petstore/kotlin-springboot-3*/**'
- 'samples/server/petstore/kotlin-server/**'
- 'samples/server/petstore/kotlin-server-modelMutable/**'
Expand All @@ -34,6 +36,10 @@ jobs:
matrix:
sample:
# server
- samples/server/others/kotlin-server/polymorphism-allof-and-discriminator
- samples/server/others/kotlin-server/polymorphism-and-discriminator-disabled-jackson-fix
- samples/server/others/kotlin-server/polymorphism-and-discriminator
- samples/server/others/kotlin-server/polymorphism
- samples/server/petstore/kotlin-server-required-and-nullable-properties
- samples/server/petstore/kotlin-springboot-3
- samples/server/petstore/kotlin-springboot-3-no-response-entity
Expand Down
6 changes: 6 additions & 0 deletions .github/workflows/samples-kotlin-server-jdk21.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -3,10 +3,12 @@ name: Samples Kotlin server (jdk21)
on:
push:
paths:
- 'samples/server/others/kotlin-server/**'
- 'samples/server/petstore/kotlin-server/**'
- 'samples/server/petstore/kotlin-server-required-and-nullable-properties/**'
pull_request:
paths:
- 'samples/server/others/kotlin-server/**'
- 'samples/server/petstore/kotlin-server/**'
- 'samples/server/petstore/kotlin-server-required-and-nullable-properties/**'

Expand All @@ -21,6 +23,10 @@ jobs:
fail-fast: false
matrix:
sample:
- samples/server/others/kotlin-server/polymorphism-allof-and-discriminator
- samples/server/others/kotlin-server/polymorphism-and-discriminator-disabled-jackson-fix
- samples/server/others/kotlin-server/polymorphism-and-discriminator
- samples/server/others/kotlin-server/polymorphism
- samples/server/petstore/kotlin-server/javalin-6
- samples/server/petstore/kotlin-server/ktor
- samples/server/petstore/kotlin-server/ktor2
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
generatorName: kotlin-server
outputDir: samples/server/others/kotlin-server/polymorphism-allof-and-discriminator
inputSpec: modules/openapi-generator/src/test/resources/3_1/polymorphism-allof-and-discriminator.yaml
templateDir: modules/openapi-generator/src/main/resources/kotlin-server
additionalProperties:
artifactId: kotlin-server-polymorphism-allof-and-discriminator
library: javalin6
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
generatorName: kotlin-server
outputDir: samples/server/others/kotlin-server/polymorphism-and-discriminator-disabled-jackson-fix
inputSpec: modules/openapi-generator/src/test/resources/3_1/polymorphism-and-discriminator.yaml
templateDir: modules/openapi-generator/src/main/resources/kotlin-server
additionalProperties:
artifactId: kotlin-server-polymorphism-and-discriminator-disabled-jackson-fix
library: javalin6
# This is set to "true" by default, but we also want to test the case where it's set to false.
# See KotlinServerCodegen.java for more details about this property.
fixJacksonJsonTypeInfoInheritance: false
8 changes: 8 additions & 0 deletions bin/configs/kotlin-server-polymorphism-and-discriminator.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
generatorName: kotlin-server
outputDir: samples/server/others/kotlin-server/polymorphism-and-discriminator
inputSpec: modules/openapi-generator/src/test/resources/3_1/polymorphism-and-discriminator.yaml
templateDir: modules/openapi-generator/src/main/resources/kotlin-server
additionalProperties:
artifactId: kotlin-server-polymorphism-and-discriminator
library: javalin6
fixJacksonJsonTypeInfoInheritance: true
7 changes: 7 additions & 0 deletions bin/configs/kotlin-server-polymorphism.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
generatorName: kotlin-server
outputDir: samples/server/others/kotlin-server/polymorphism
inputSpec: modules/openapi-generator/src/test/resources/3_1/polymorphism.yaml
templateDir: modules/openapi-generator/src/main/resources/kotlin-server
additionalProperties:
artifactId: kotlin-server-polymorphism
library: javalin6
7 changes: 4 additions & 3 deletions docs/generators/kotlin-server.md
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,7 @@ These options may be applied as additional-properties (cli) or configOptions (pl
|featureHSTS|Avoid sending content if client already has same content, by checking ETag or LastModified properties.| |true|
|featureMetrics|Enables metrics feature.| |true|
|featureResources|Generates routes in a typed way, for both: constructing URLs and reading the parameters.| |true|
|fixJacksonJsonTypeInfoInheritance|When true (default), ensures Jackson polymorphism works correctly by: (1) always setting visible=true on @JsonTypeInfo, and (2) adding the discriminator property to child models with appropriate default values. When false, visible is only set to true if all children already define the discriminator property.| |true|
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is where things get tricky. Let's take this schema as an example:

Pet schema
# https://spec.openapis.org/oas/v3.2.0.html#models-with-polymorphism-support-and-a-discriminator-object
# 4.24.8.8 Models with Polymorphism Support and a Discriminator Object
# The following example extends the example of the previous section by adding a Discriminator Object to the Pet schema. Note that the Discriminator Object is only a hint to the consumer of the API and does not change the validation outcome of the schema.
openapi: 3.1.0
info:
  title: Basic polymorphism example with discriminator
  version: "1.0"
components:
  schemas:
    Pet:
      type: object
      discriminator:
        propertyName: petType
        mapping:
          cat: '#/components/schemas/Cat'
          dog: '#/components/schemas/Dog'
      properties:
        name:
          type: string
      required:
        - name
        - petType
      oneOf:
        - $ref: '#/components/schemas/Cat'
        - $ref: '#/components/schemas/Dog'
    Cat:
      description: A pet cat
      type: object
      properties:
        petType:
          const: 'cat'
        huntingSkill:
          type: string
          description: The measured skill for hunting
          enum:
            - clueless
            - lazy
            - adventurous
            - aggressive
      required:
        - huntingSkill
    Dog:
      description: A pet dog
      type: object
      properties:
        petType:
          const: 'dog'
        packSize:
          type: integer
          format: int32
          description: the size of the pack the dog is from
          default: 0
          minimum: 0
      required:
        - petType
        - packSize

What the spec says about discriminator and petType

The discriminator is only a hint and does not change validation

In section 4.24.8.8, the spec explicitly says:

“the Discriminator Object is only a hint … and does not change the validation outcome of the schema.”

This means discriminator by itself doesn’t automatically “require” petType or enforce constraints.

Does the discriminator property have to be present in the payload?

Yes, if the parent schema requires it

In our Pet schema:

required:
  - name
  - petType

So the schema rules say: a valid Pet instance must contain petType, even if the discriminator is "just a hint." The required constraint is what makes it mandatory.

Also, in the discriminator examples section the spec states:

"The expectation now is that a property with name petType MUST be present in the response payload …"

So: yes, petType must be present in the payload, because the parent schema requires it and the discriminator usage assumes it exists.

openapi-generator behavior - with and without the Jackson fix applied

Without the fix, the generated code looks like this:

Without the fix
/**
 * Basic polymorphism example with discriminator
 * No description provided (generated by Openapi Generator https://github.com/openapitools/openapi-generator)
 *
 * The version of the OpenAPI document: 1.0
 * 
 *
 * NOTE: This class is auto generated by OpenAPI Generator (https://openapi-generator.tech).
 * https://openapi-generator.tech
 * Do not edit the class manually.
*/
package org.openapitools.server.models

import org.openapitools.server.models.Cat
import org.openapitools.server.models.Dog

/**
 * 
 */
@com.fasterxml.jackson.annotation.JsonTypeInfo(use = com.fasterxml.jackson.annotation.JsonTypeInfo.Id.NAME, include = com.fasterxml.jackson.annotation.JsonTypeInfo.As.PROPERTY, property = "petType", visible = false)
@com.fasterxml.jackson.annotation.JsonSubTypes(
    com.fasterxml.jackson.annotation.JsonSubTypes.Type(value = Cat::class, name = "cat"),
    com.fasterxml.jackson.annotation.JsonSubTypes.Type(value = Dog::class, name = "dog")
)
sealed class Pet
/**
 * Basic polymorphism example with discriminator
 * No description provided (generated by Openapi Generator https://github.com/openapitools/openapi-generator)
 *
 * The version of the OpenAPI document: 1.0
 * 
 *
 * NOTE: This class is auto generated by OpenAPI Generator (https://openapi-generator.tech).
 * https://openapi-generator.tech
 * Do not edit the class manually.
*/
package org.openapitools.server.models


/**
 * A pet cat
 * @param huntingSkill The measured skill for hunting
 * @param petType 
 */
data class Cat(
    /* The measured skill for hunting */
    
    @field:com.fasterxml.jackson.annotation.JsonProperty("huntingSkill")
    val huntingSkill: Cat.HuntingSkill,
    
    @field:com.fasterxml.jackson.annotation.JsonProperty("petType")
    val petType: kotlin.Any? = null
) : Pet()
{
    /**
    * The measured skill for hunting
    * Values: CLUELESS,LAZY,ADVENTUROUS,AGGRESSIVE
    */
    enum class HuntingSkill(val value: kotlin.String){
        CLUELESS("clueless"),
        LAZY("lazy"),
        ADVENTUROUS("adventurous"),
        AGGRESSIVE("aggressive");
    }
}
/**
 * Basic polymorphism example with discriminator
 * No description provided (generated by Openapi Generator https://github.com/openapitools/openapi-generator)
 *
 * The version of the OpenAPI document: 1.0
 * 
 *
 * NOTE: This class is auto generated by OpenAPI Generator (https://openapi-generator.tech).
 * https://openapi-generator.tech
 * Do not edit the class manually.
*/
package org.openapitools.server.models


/**
 * A pet dog
 * @param petType 
 * @param packSize the size of the pack the dog is from
 */
data class Dog(
    
    @field:com.fasterxml.jackson.annotation.JsonProperty("petType")
    val petType: kotlin.Any?,
    /* the size of the pack the dog is from */
    
    @field:com.fasterxml.jackson.annotation.JsonProperty("packSize")
    val packSize: kotlin.Int = 0
) : Pet()

As you can see:
🧐 petType is not a property of the parent/sealed Pet class. While this works, Jackson does require petType in the JSON payload for being able to deserialize, so we might as well add it as a property to the sealed class for convenience. Note that we need visible = true in the JsonTypeInfo annotation for Jackson to populate this value.
🧐 the petType property being present in the children classes (Dog and Cat) now fully depends on whether they're defined in the YAML spec. In this case they are, but it's optional in Cat and required in Dog. And it's nullable in both cases. While the code generator correctly generated code according to spec here, its practical value is highly questionable, because Jackson requires petType to always be provided in order to be able to properly deserialize.

With the fix applied, it looks like this:

With the fix applied
/**
 * Basic polymorphism example with discriminator
 * No description provided (generated by Openapi Generator https://github.com/openapitools/openapi-generator)
 *
 * The version of the OpenAPI document: 1.0
 * 
 *
 * NOTE: This class is auto generated by OpenAPI Generator (https://openapi-generator.tech).
 * https://openapi-generator.tech
 * Do not edit the class manually.
*/
package org.openapitools.server.models

import org.openapitools.server.models.Cat
import org.openapitools.server.models.Dog

/**
 * 
 * @param petType 
 */
@com.fasterxml.jackson.annotation.JsonTypeInfo(use = com.fasterxml.jackson.annotation.JsonTypeInfo.Id.NAME, include = com.fasterxml.jackson.annotation.JsonTypeInfo.As.PROPERTY, property = "petType", visible = true)
@com.fasterxml.jackson.annotation.JsonSubTypes(
    com.fasterxml.jackson.annotation.JsonSubTypes.Type(value = Cat::class, name = "cat"),
    com.fasterxml.jackson.annotation.JsonSubTypes.Type(value = Dog::class, name = "dog")
)
sealed class Pet(
    
    @field:com.fasterxml.jackson.annotation.JsonProperty("petType")
    open val petType: kotlin.String

)
/**
 * Basic polymorphism example with discriminator
 * No description provided (generated by Openapi Generator https://github.com/openapitools/openapi-generator)
 *
 * The version of the OpenAPI document: 1.0
 * 
 *
 * NOTE: This class is auto generated by OpenAPI Generator (https://openapi-generator.tech).
 * https://openapi-generator.tech
 * Do not edit the class manually.
*/
package org.openapitools.server.models


/**
 * A pet cat
 * @param huntingSkill The measured skill for hunting
 * @param petType 
 */
data class Cat(
    /* The measured skill for hunting */
    
    @field:com.fasterxml.jackson.annotation.JsonProperty("huntingSkill")
    val huntingSkill: Cat.HuntingSkill,
    
    @field:com.fasterxml.jackson.annotation.JsonProperty("petType")
    override val petType: kotlin.String = "cat",

) : Pet(petType = petType)
{
    /**
    * The measured skill for hunting
    * Values: CLUELESS,LAZY,ADVENTUROUS,AGGRESSIVE
    */
    enum class HuntingSkill(val value: kotlin.String){
        CLUELESS("clueless"),
        LAZY("lazy"),
        ADVENTUROUS("adventurous"),
        AGGRESSIVE("aggressive");
    }
}
/**
 * Basic polymorphism example with discriminator
 * No description provided (generated by Openapi Generator https://github.com/openapitools/openapi-generator)
 *
 * The version of the OpenAPI document: 1.0
 * 
 *
 * NOTE: This class is auto generated by OpenAPI Generator (https://openapi-generator.tech).
 * https://openapi-generator.tech
 * Do not edit the class manually.
*/
package org.openapitools.server.models


/**
 * A pet dog
 * @param petType 
 * @param packSize the size of the pack the dog is from
 */
data class Dog(
    
    @field:com.fasterxml.jackson.annotation.JsonProperty("petType")
    override val petType: kotlin.String = "dog",
    /* the size of the pack the dog is from */
    
    @field:com.fasterxml.jackson.annotation.JsonProperty("packSize")
    val packSize: kotlin.Int = 0
) : Pet(petType = petType)

As you can see:
petType is now a required, non-nullable property on the parent/sealed Pet class. Since Jackson already requires it to be set for JsonTypeInfo to be able to deserialize things properly, and visible = true, we can be assured that the property will always be set by Jackson.
✅ The petType property is now present as a required, non-nullable property in the children classes (Dog and Cat). As mentioned above, Jackson already requires this in order to be able to deserialize properly.

This might not be desired behavior in all cases

Let's say I want to re-use the Cat class elsewhere without using the discriminator, it might not be expected that petType is now a required, non-nullable field on the Kotlin data class, because the spec says that it's not required for the Cat model. It's probably not the best example, but I hope you get my train of thought.

For this reason, I wanted to make this behavior configurable. But to be honest, it feels like a bit of an edge case, and we might as well not make it configurable (always applying this fix for Jackson). Curious what y'all think about this.

|groupId|Generated artifact package's organization (i.e. maven groupId).| |org.openapitools|
|interfaceOnly|Whether to generate only API interface stubs without the server files. This option is currently supported only when using jaxrs-spec library.| |false|
|library|library template (sub-template)|<dl><dt>**ktor**</dt><dd>ktor framework</dd><dt>**ktor2**</dt><dd>ktor (2.x) framework</dd><dt>**jaxrs-spec**</dt><dd>JAX-RS spec only</dd><dt>**javalin5**</dt><dd>Javalin 5</dd><dt>**javalin6**</dt><dd>Javalin 6</dd></dl>|ktor|
Expand Down Expand Up @@ -262,11 +263,11 @@ These options may be applied as additional-properties (cli) or configOptions (pl
| ---- | --------- | ---------- |
|Simple|✓|OAS2,OAS3
|Composite|✓|OAS2,OAS3
|Polymorphism||OAS2,OAS3
|Polymorphism||OAS2,OAS3
|Union|✗|OAS3
|allOf||OAS2,OAS3
|allOf||OAS2,OAS3
|anyOf|✗|OAS3
|oneOf||OAS3
|oneOf||OAS3
|not|✗|OAS3

### Security Feature
Expand Down
Loading
Loading