OpenAPITools / openapi-generator

OpenAPI Generator allows generation of API client libraries (SDK generation), server stubs, documentation and configuration automatically given an OpenAPI Spec (v2, v3)
https://openapi-generator.tech
Apache License 2.0
21.84k stars 6.59k forks source link

[BUG] Kotlin DTO with object member not generated properly #17658

Open PierreMardon opened 9 months ago

PierreMardon commented 9 months ago

Bug Report Checklist

Description
openapi-generator version

7.2.0, don't know about previous versions as this was my first attempt

OpenAPI declaration file content or url

With this schema :

openapi: 3.0.0
paths:
info:
  title: Eixample API
  description: The API for the Eixample backend.
  version: 0.1.0
servers:
  - description: Development
    url: https://api.dev.eixample.dev
components:
  securitySchemes:
    bearer:
      type: http
      scheme: bearer
      bearerFormat: JWT
  schemas:
    Geozone:
      type: object
      properties:
        geoJson:
          type: object
          description: The definition of the geometry for the geozone.
          additionalProperties: true
        id:
          type: string
          description: The unique identifier for the geozone.
          format: uuid
      required:
        - geoJson
        - id

I obtain this class:

/**
 *
 * Please note:
 * This class is auto generated by OpenAPI Generator (https://openapi-generator.tech).
 * Do not edit this file manually.
 *
 */

@file:Suppress(
    "ArrayInDataClass",
    "EnumEntryName",
    "RemoveRedundantQualifierName",
    "UnusedImport"
)

package com.heetch.common.business.repositories.eixampleApi.dto

import com.heetch.common.business.repositories.eixampleApi.dto.AllowedLocation

import kotlinx.serialization.Serializable
import kotlinx.serialization.SerialName
import kotlinx.serialization.Contextual

/**
 * 
 *
 * @param geoJson The definition of the geometry for the geozone.
 * @param id The unique identifier for the geozone.
 */
@Serializable

data class Geozone (

    /* The definition of the geometry for the geozone. */
    @Contextual @SerialName(value = "geoJson")
    val geoJson: kotlin.collections.Map<kotlin.String, kotlin.Any>,

    /* The unique identifier for the geozone. */
    @Contextual @SerialName(value = "id")
    val id: java.util.UUID,

Which causes an error on val geoJson: kotlin.collections.Map<kotlin.String, kotlin.Any> described as:

Serializer has not been found for type 'Any'. To use context serializer as fallback, explicitly annotate type or property with @Contextual
Generation Details

Gradle plugin task:

openApiGenerate {
    generatorName.set("kotlin")
    inputSpec.set("$rootDir/some.yaml")
    outputDir.set(generatedSourcesPath)
    packageName.set("some.package")
    modelPackage.set("some.package")
    configOptions.set(mapOf(
        "dateLibrary" to "java8",
        "collectionType" to "list",
        "serializationLibrary" to "kotlinx_serialization",
    ))
}
Steps to reproduce

Execute the openApiGenerate task

Related issues/PRs
Suggest a fix

Adding @Contextual to the second type of Map : kotlin.collections.Map<kotlin.String, @Contextual kotlin.Any> seems to fix the issue.

PierreMardon commented 9 months ago

In fact, my previous suggestion won't work. Replacing the Map with kotlinx.serialization.json.JsonObject works and seems appropriate.

charlee-dev commented 6 months ago

How did you doReplacing the Map with kotlinx.serialization.json.JsonObject? @PierreMardon

PierreMardon commented 6 months ago

@charlee-dev I'm far from being an expert so there may be a better way to do so, but as I needed multiple post-generation tweaks I implemented a copy task:

tasks.register<Copy>("postProcessDto") {
    from(generatedDtoFolder)
    into(targetDtoFolder)
    filter { line: String ->
        line
            // Fix for https://github.com/OpenAPITools/openapi-generator/issues/17658
            .replace(
                "kotlin.collections.Map<kotlin.String, kotlin.Any>",
                "kotlinx.serialization.json.JsonObject"
            )
            // We need plain old java Date
            .replace("java.time.OffsetDateTime", "java.util.Date")
    }
    dependsOn("openApiGenerate")
}
mtrakal commented 1 week ago

In case, that you don't need to copy DTOs to different folder, you can use edited task:

val dtoPath = "$projectDir/src/main/kotlin/.../dto"

/**
 * Post process DTO files generated by OpenAPI generator.
 * We need to replace `kotlin.collections.Map<kotlin.String, kotlin.Any>` with `kotlinx.serialization.json.JsonObject`
 * due to missing serialization for kotlin.Any.
 */
tasks.register("postProcessDto") {
    group = "openapi tools"
    fileTree(dtoPath)
        .matching {
            include("**/*.kt")
        }.forEach { file ->
            if (file.isFile) {
                val content = file.readText()
                val updatedContent = content.replace(
                    "kotlin.collections.Map<kotlin.String, kotlin.Any>",
                    "kotlinx.serialization.json.JsonObject",
                )
                file.writeText(updatedContent)
            }
        }
}

Should do the same (except replacing Date, because we are using kotlinx.datetime)