0

I’ve built a Spring Boot REST API that exposes an endpoint returning a list of animals (/animals). The API supports multiple animal subtypes (Cat, Bird), each with unique fields, and I’ve modeled them in Kotlin using a sealed class hierarchy to represent inheritance.

Each Animal subclass (like AnimalCat or AnimalBird) extends a base Animal class, and I expose their DTO equivalents (AnimalV1Dto.AnimalCatV1Dto, AnimalV1Dto.AnimalBirdV1Dto) in the Swagger documentation.

The OpenAPI documentation correctly shows the polymorphic structure using:

  • oneOf for subtype schemas
  • discriminatorProperty = "type"

However, when another application uses openapi-generator-maven-plugin to generate models from my Swagger docs, the generated Kotlin classes lose the inheritance hierarchy — instead of AnimalCatV1Dto and AnimalBirdV1Dto extending AnimalV1Dto, they are generated as independent classes.

My goal is for the OpenAPI generator to produce models like:

@JsonTypeInfo(use = JsonTypeInfo.Id.NAME, property = "type")
@JsonSubTypes(
    JsonSubTypes.Type(value = AnimalBirdV1Dto::class, name = "Bird"),
    JsonSubTypes.Type(value = AnimalCatV1Dto::class, name = "Cat")
)
open class AnimalV1Dto()

data class AnimalCatV1Dto(...) : AnimalV1Dto()
data class AnimalBirdV1Dto(...) : AnimalV1Dto()

…but instead it currently generates both as standalone data classes, losing the extends AnimalV1Dto relationship:


import com.fasterxml.jackson.annotation.JsonProperty

/**
 * 
 *
 * @param name 
 * @param weight 
 * @param height 
 * @param ageInCatYears 
 * @param type 
 */


data class AnimalCatV1Dto (

    @get:JsonProperty("name")
    val name: kotlin.String,

    @get:JsonProperty("weight")
    val weight: kotlin.Int,

    @get:JsonProperty("height")
    val height: kotlin.Int,

    @get:JsonProperty("ageInCatYears")
    val ageInCatYears: kotlin.Int,

    @get:JsonProperty("type")
    val type: AnimalCatV1Dto.Type

) {

    /**
     * 
     *
     * Values: Cat,Bird
     */
    enum class Type(val value: kotlin.String) {
        @JsonProperty(value = "Cat") Cat("Cat"),
        @JsonProperty(value = "Bird") Bird("Bird");
    }

}

data class AnimalBirdV1Dto (

    @get:JsonProperty("name")
    val name: kotlin.String,

    @get:JsonProperty("weight")
    val weight: kotlin.Int,

    @get:JsonProperty("height")
    val height: kotlin.Int,

    @get:JsonProperty("wingSpan")
    val wingSpan: kotlin.Int,

    @get:JsonProperty("type")
    val type: AnimalBirdV1Dto.Type

) {

    /**
     * 
     *
     * Values: Cat,Bird
     */
    enum class Type(val value: kotlin.String) {
        @JsonProperty(value = "Cat") Cat("Cat"),
        @JsonProperty(value = "Bird") Bird("Bird");
    }

}

The application that is going to use my API uses openapi-generator-maven-plugin in order to automatically generate models based on the Swagger Docs using the following under in pom.xml:


            <plugin>
                <groupId>org.openapitools</groupId>
                <artifactId>openapi-generator-maven-plugin</artifactId>
                <version>7.16.0</version>
                <executions>

                    <execution>
                        <id>generate-animals-model</id>
                        <goals>
                            <goal>generate</goal>
                        </goals>
                        <configuration>
                            <inputSpec>http://localhost:8010/v3/api-docs</inputSpec>
                            <modelPackage>com.github.ditlef9.animalsapi.rest.dto</modelPackage>
                            <generatorName>kotlin</generatorName>
                            <library>jvm-okhttp4</library>

                            <generateModels>true</generateModels>

                            <generateModelTests>false</generateModelTests>
                            <generateApis>false</generateApis>
                            <generateModelDocumentation>false</generateModelDocumentation>
                            <generateApiTests>false</generateApiTests>
                            <generateApiDocumentation>false</generateApiDocumentation>
                            <generateSupportingFiles>false</generateSupportingFiles>
                            <typeMappings>
                                <typeMapping>DateTime=Instant</typeMapping>
                                <typeMapping>Date=Date</typeMapping>
                            </typeMappings>
                            <importMappings>
                                <importMapping>Instant=java.time.Instant</importMapping>
                                <importMapping>Date=java.util.Date</importMapping>
                                <importMapping>LocalDate=java.time.LocalDate</importMapping>
                            </importMappings>
                            <configOptions>
                                <serializationLibrary>jackson</serializationLibrary>
                                <useJakartaEe>true</useJakartaEe>
                                <useBeanValidation>true</useBeanValidation>
                                <sourceFolder>/src/main/kotlin</sourceFolder>
                            </configOptions>
                        </configuration>
                    </execution>
                </executions>
            </plugin>

My API:

model/Animal:

import com.github.ditlef9.animalsapi.dto.AnimalV1Dto

/**
 * Represents a generic animal with shared properties like [name], [weight], and [height].
 *
 * Subclasses represent specific animal types with additional attributes.
 */
sealed class Animal(
    open val name: String,
    open val weight: Int,
    open val height: Int
) {

    /**
     * Represents a cat with an additional property [ageInCatYears].
     *
     * Use [tryCreate] to safely construct an instance, ensuring all parameters are non-null.
     *
     * Example:
     * ```
     * val cat = Animal.AnimalCat.tryCreate("Misty", 5, 30, 3)
     * ```
     */
    data class AnimalCat private constructor(
        override val name: String,
        override val weight: Int,
        override val height: Int,
        val ageInCatYears: Int
    ) : Animal(name, weight, height) {

        companion object {
            fun tryCreate(
                name: String?,
                weight: Int?,
                height: Int?,
                ageInCatYears: Int?
            ): AnimalCat {
                requireNotNull(name) { "name must not be null" }
                requireNotNull(weight) { "weight must not be null" }
                requireNotNull(height) { "height must not be null" }
                requireNotNull(ageInCatYears) { "ageInCatYears must not be null" }

                return AnimalCat(name, weight, height, ageInCatYears)
            }
        }

        fun toDto(): AnimalV1Dto.AnimalCatV1Dto =
            AnimalV1Dto.AnimalCatV1Dto(
                name = name,
                weight = weight,
                height = height,
                ageInCatYears = ageInCatYears
            )
    }

    /**
     * Represents a bird with an additional property [wingSpan], measured in centimeters.
     *
     * Use [tryCreate] to safely construct an instance, ensuring all parameters are non-null.
     *
     * Example:
     * ```
     * val bird = Animal.AnimalBird.tryCreate("Robin", 1, 15, 25)
     * ```
     */
    data class AnimalBird private constructor(
        override val name: String,
        override val weight: Int,
        override val height: Int,
        val wingSpan: Int // in centimeters
    ) : Animal(name, weight, height) {

        companion object {
            fun tryCreate(
                name: String?,
                weight: Int?,
                height: Int?,
                wingSpan: Int?
            ): AnimalBird {
                requireNotNull(name) { "name must not be null" }
                requireNotNull(weight) { "weight must not be null" }
                requireNotNull(height) { "height must not be null" }
                requireNotNull(wingSpan) { "wingSpan must not be null" }

                return AnimalBird(name, weight, height, wingSpan)
            }
        }

        fun toDto(): AnimalV1Dto.AnimalBirdV1Dto =
            AnimalV1Dto.AnimalBirdV1Dto(
                name = name,
                weight = weight,
                height = height,
                wingSpan = wingSpan
            )

    }


    companion object {
        fun List<Animal>.toDto(): List<AnimalV1Dto> =
            this.map {
                when (it) {
                    is AnimalCat -> it.toDto()
                    is AnimalBird -> it.toDto()
                }
            }
    }
}

dto/AnimalV1Dto:



sealed class AnimalV1Dto(
    open val name: String,
    open val weight: Int,
    open val height: Int,
    val type: AnimalTypeV1Dto
) {

    data class AnimalCatV1Dto(
        override val name: String,
        override val weight: Int,
        override val height: Int,
        val ageInCatYears: Int
    ) : AnimalV1Dto(name, weight, height, AnimalTypeV1Dto.Cat)

    data class AnimalBirdV1Dto(
        override val name: String,
        override val weight: Int,
        override val height: Int,
        val wingSpan: Int
    ) : AnimalV1Dto(name, weight, height, AnimalTypeV1Dto.Bird)
}

enum class AnimalTypeV1Dto(
    val value: String,
) {
    Cat("Cat"),
    Bird("Bird");

    override fun toString(): String = value
}

controller/AnimalsController


import com.github.ditlef9.animalsapi.dto.AnimalV1Dto
import com.github.ditlef9.animalsapi.service.AnimalService
import io.swagger.v3.oas.annotations.Operation
import io.swagger.v3.oas.annotations.media.*
import io.swagger.v3.oas.annotations.responses.ApiResponse
import io.swagger.v3.oas.annotations.responses.ApiResponses
import io.swagger.v3.oas.annotations.tags.Tag
import org.springframework.http.MediaType.APPLICATION_JSON_VALUE
import org.springframework.web.bind.annotation.GetMapping
import org.springframework.web.bind.annotation.RestController

@RestController
@Tag(name = "Animals", description = "Operations related to animals")
class AnimalsController(
    private val animalService: AnimalService
) {

    @GetMapping("/animals")
    @Operation(summary = "Get all animals", description = "Returns a list of all animals, including cats and birds")
    @ApiResponses(
        value = [
            ApiResponse(
                responseCode = "200",
                description = "OK",
                content = [
                    Content(
                        mediaType = APPLICATION_JSON_VALUE,
                        array = ArraySchema(
                            schema = Schema(
                                oneOf = [
                                    AnimalV1Dto.AnimalCatV1Dto::class,
                                    AnimalV1Dto.AnimalBirdV1Dto::class,
                                ],
                                discriminatorProperty = "type",
                                discriminatorMapping = [
                                    DiscriminatorMapping(
                                        value = "Cat",
                                        schema = AnimalV1Dto.AnimalCatV1Dto::class,
                                    ),
                                    DiscriminatorMapping(
                                        value = "Bird",
                                        schema = AnimalV1Dto.AnimalBirdV1Dto::class,
                                    ),
                                ],
                                title = "AnimalV1Dto",
                                description = "Represents an animal of type Cat or Bird",
                            ),
                        ),
                        examples = [
                            ExampleObject(value = ApiResponseExamples.LIST_OF_ANIMALV1_DTO_EXAMPLE)
                        ]
                    ),
                ],
            ),
        ],
    )
    fun getAnimals(): List<AnimalV1Dto> =
        animalService.getAllAnimals()
}

// API Response
object ApiResponseExamples {
    const val LIST_OF_ANIMALV1_DTO_EXAMPLE = """[
      {
        "name": "Misty",
        "weight": 5,
        "height": 30,
        "ageInCatYears": 3,
        "type": "Cat"
      },
      {
        "name": "Robin",
        "weight": 1,
        "height": 15,
        "wingSpan": 25,
        "type": "Bird"
      }
    ]"""
}

serice/AnimalService


import com.github.ditlef9.animalsapi.dto.AnimalV1Dto
import com.github.ditlef9.animalsapi.model.Animal
import com.github.ditlef9.animalsapi.model.Animal.Companion.toDto
import org.springframework.stereotype.Service

@Service
class AnimalService {
    private val animals: List<Animal> = listOf(
        Animal.AnimalCat.tryCreate("Misty", 5, 30, 3),
        Animal.AnimalBird.tryCreate("Robin", 1, 15, 25)
    )

    fun getAllAnimals(): List<AnimalV1Dto> {

        return animals.toDto()

    }
}

0

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.