SMILEY4 / ktor-swagger-ui

Kotlin Ktor plugin to generate OpenAPI and provide Swagger UI
Apache License 2.0
150 stars 25 forks source link

Issues with generated schemas #75

Closed ma7moudat closed 2 months ago

ma7moudat commented 8 months ago

Hey,

Thanks for the nice library. It was an easy setup!

This is more of a nice-to-have, but would be immensely helpful in my case.

I was able to configure the library in a Ktor project and the documentation is being properly generated from the response body types I supply.

The problem: I want to use the resulting JSON to generate an API client in the frontend using @openapitools/openapi-generator-cli. This was possible while I was manually editing the spec file, but now in the generated spec I found 2 issues with the resulting schemas (check comments in this snippet):

{
  "components": {
    "schemas": {
      // === Map usage
      "Map(DayOfWeek,String)": {
        "type": "object",
        "additionalProperties": {
          "type": "string"
        }
      },
      "ScheduleData": {
        "schedule": {
          "$ref": "#/components/schemas/Map(DayOfWeek,String)"
        }
      }
    },
    // === Compelete import paths
    "com.project.util.RecordsPage<com.project.schedule.ScheduleData>": {
      "type": "object",
      "properties": {
        "meta": {
          "$ref": "#/components/schemas/PageMeta"
        },
        "records": {
          "type": "array",
          "items": {
            "$ref": "#/components/schemas/ScheduleData"
          }
        }
      }
    }
  }
}

These issues prevent the generation of API client with the following errors:

Exception in thread "main" org.openapitools.codegen.SpecValidationException: There were issues with the specification. The option can be disabled via validateSpec (Maven/Gradle) or --skip-validate-spec (CLI).
 | Error count: 3, Warning count: 0
Errors: 
        -components.schemas.Schema name Map(DayOfWeek,String) doesn't adhere to regular expression ^[a-zA-Z0-9\.\-_]+$
        -components.schemas.Schema name sit.adcommon.util.RecordsPage<sit.adcommon.unpackingTime.UnpackingTimeData> doesn't adhere to regular expression ^[a-zA-Z0-9\.\-_]+$

Can these be solved somehow? Maybe by providing an alias for the response type instead of having function calls and long imports?

Thanks, Mahmoud

SMILEY4 commented 8 months ago

Hi, Thank you!

it adds the raw/complete name (including the package path) when the type has a generic type. I don't remember the reason for this behaviour, but it should be possible to change. If not, i think the alias is a good idea - maybe even in general without the long name problem.

I'll look into it.

douglasqueirozalmeida commented 8 months ago

I have the same problem. Do you have any alternative solution? Or wait for a new release? @SMILEY4

SMILEY4 commented 8 months ago

Hi, I am currently a bit confused about the api code generator.

Assuming you could give every schema in the components section its own name/alias, how would the generator handle that? For example you would create an alias ScheduleMapping for Map(DayOfWeek,String), what class/type would the generator create from that? How would it know its a map? Same with RecordsPage and the generic type?

Currently, it looks like automatically creating clean names isnt possible and even attacking aliases to everything is not that easy (e.g. afaik, the Map(DayOfWeek,String) is a nested field and the name comes from the schema-generation-library), so i'am trying to understand the problem better and how to solve it best.

Thank you

douglasqueirozalmeida commented 8 months ago

@SMILEY4 I made some test APIs to demonstrate how the file is generated and what is presented when validating via Swagger. Disregard the names of classes and methods. Just to demonstrate and facilitate understanding.

 data class CfcResponse<T>(
    val data: T? = null,
    val message: String? = null
)

fun Route.test() {

    route("test") {
        post({
            response {
                HttpStatusCode.OK to {
                    description = ""
                    body<Map<String, LoginRequest>>()
                }
            }
        }) { }

        post({
            response {
                HttpStatusCode.OK to {
                    description = ""
                    body<Map<String, String>>()
                }
            }
        }) { }

        post({
            response {
                HttpStatusCode.OK to {
                    description = ""
                    body<CfcResponse<LoginRequest>>()
                }
            }
        }) { }
    }
}

YAML/JSON

openapi: 3.0.1
info:
  title: TEST API
  version: '1.0'
externalDocs:
  url: /
servers:
  - url: http://localhost:8080
    description: Development server
tags: []
paths:
  /v1/login:
    post:
      tags: []
      parameters: []
      requestBody:
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/LoginRequest'
      responses:
        '200':
          description: token
          headers: {}
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/br.com.infiniteloop.domain.model.response.CfcResponse<kotlin.String>'
      deprecated: false
  /v1/user:
    put:
      tags: []
      parameters: []
      requestBody:
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/UserUpdateRequest'
      responses:
        '204':
          description: no content
          headers: {}
      deprecated: false
  /v1/test:
    post:
      tags: []
      parameters: []
      responses:
        '200':
          description: ''
          headers: {}
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/br.com.infiniteloop.domain.model.response.CfcResponse<br.com.infiniteloop.domain.model.request.LoginRequest>'
      deprecated: false
components:
  schemas:
    LoginRequest:
      type: object
      properties:
        codeAccess:
          type: string
        pass:
          type: string
    br.com.infiniteloop.domain.model.response.CfcResponse<kotlin.String>:
      type: object
      properties:
        data:
          type: string
        message:
          type: string
    UserUpdateRequest:
      type: object
      properties:
        name:
          type: string
        pass:
          type: string
        ssap:
          type: string
    kotlin.collections.Map<kotlin.String, br.com.infiniteloop.domain.model.request.LoginRequest>:
      type: object
      additionalProperties:
        $ref: '#/components/schemas/LoginRequest'
    kotlin.collections.Map<kotlin.String, kotlin.String>:
      type: object
      additionalProperties:
        type: string
    br.com.infiniteloop.domain.model.response.CfcResponse<br.com.infiniteloop.domain.model.request.LoginRequest>:
      type: object
      properties:
        data:
          $ref: '#/components/schemas/LoginRequest'
        message:
          type: string
  examples: {}

Swagger Validate

Captura de Tela 2023-12-11 às 19 49 12
Sterta commented 8 months ago

@ma7moudat as a temporary workaround you could use skipValidateSpec.set(true) in your openApiGenerate configuration (CLI --skip-validate-spec)

ma7moudat commented 8 months ago

Hi, I am currently a bit confused about the api code generator.

Assuming you could give every schema in the components section its own name/alias, how would the generator handle that? For example you would create an alias ScheduleMapping for Map(DayOfWeek,String), what class/type would the generator create from that? How would it know its a map? Same with RecordsPage and the generic type?

Currently, it looks like automatically creating clean names isnt possible and even attacking aliases to everything is not that easy (e.g. afaik, the Map(DayOfWeek,String) is a nested field and the name comes from the schema-generation-library), so i'am trying to understand the problem better and how to solve it best.

Thank you

Hi @SMILEY4,

True, you're absolutely right. It's more complex than it seems on the first look, especially with my lack of Kotlin/Ktor experience.

Some gaps are hard to bridge between Kotlin/Ktor and Typescript (or any 2 programming languages in general), but I was hoping to find a way to turn Map(DayOfWeek,String) from Kotlin into something like Map<String, String> in Typescript somehow, but that means your library and the Open API spec has to be aware of both the source and target languages, which kinda beats the purpose of having a language-agnostic API spec. That's why I suggested the alias idea, but with your insight I'm not sure it's feasible either.

It's an interesting problem to tackle!

ma7moudat commented 8 months ago

@ma7moudat as a temporary workaround you could use skipValidateSpec.set(true) in your openApiGenerate configuration (CLI --skip-validate-spec)

Hi @Sterta

Not sure how that would work!

The problem isn't with generating the spec from the Kotlin code, rather when transforming the spec back into an a Typescript API client. And since I need to generate valid code, skipping validation doesn't seem like a workaround.

@SMILEY4 maybe I just need to implement some post-processing to handle these issues myself, seems like an issue that would be a little different for every project/developer. The library shouldn't have to handle a million edge cases IMO. However, if you find the inspiration for a solution, that would rock!

Devashree commented 4 months ago

@ma7moudat how did you fix this in typescript. I am also facing same issue.