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:
oneOffor subtype schemasdiscriminatorProperty="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()
}
}