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

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.