openapi-generator icon indicating copy to clipboard operation
openapi-generator copied to clipboard

[BUG] Kotlin server does not escape special character ('$') in jackson annotation

Open jasonfagerberg-toast opened this issue 9 months ago • 9 comments

Bug Report Checklist

  • [x] Have you provided a full/minimal spec to reproduce the issue?
  • [x] Have you validated the input using an OpenAPI validator (example)?
  • [x] Have you tested with the latest master to confirm the issue still exists?
  • [x] Have you searched for related issues/PRs?
  • [x] What's the actual output vs expected output?
  • [ ] [Optional] Sponsorship to speed up the bug fix or feature request (example)
Description

The given schema component with a special character in a property name.

(Example: "Group" schema from SCIM)

Using the kotlin server generator with jackson serializer generates invalid code

data class BaseGroupMembersInner (

    /* The `id` of the member.  Each value MUST be the `id` of the corresponding `User` resource, which is unique across the service provider's entire set of resources.  */

    @JsonProperty("value")
    val `value`: java.util.UUID,

    /* The URI of the corresponding `User` resource.  Nullable for requests but required for responses.  */

    @JsonProperty("$ref")
    val dollarRef: kotlin.String? = null

)

It should escape the special character ($)

@JsonProperty("\$ref")

openapi-generator version

7.11.0

OpenAPI declaration file content or url
openapi: 3.1.0

info:
  version: 5.0.5
  title: Internal Users SCIM
  description: ...
tags:
  - name: Groups
    description: ...

paths:
  /v3/internal/scim/Groups/{id}:
    get:
      summary: 'Get group by id'
      description: ...
      externalDocs:
        url: 'https://datatracker.ietf.org/doc/html/rfc7644#section-3.2'
        description: 'RFC 7644, Section 3.2'
      operationId: getGroup
      tags:
        - Groups
      parameters:
        - $ref: '#/components/parameters/GroupIdInPath'
      responses:
        '200':
          description: Successful response
          content:
            application/scim+json:
              schema:
                $ref: '#/components/schemas/Group'
            application/json:
              schema:
                $ref: '#/components/schemas/Group'

components:
  parameters:
    GroupIdInPath:
      name: id
      in: path
      description: |
        The unique identifier of a group
      required: true
      schema:
        type: string
        format: uuid
  schemas:
    Meta:
      type: object
      description: ...
      externalDocs:
        url: 'https://datatracker.ietf.org/doc/html/rfc7643#section-3.1'
        description: 'RFC 7643, Section 3.1'
      properties:
        resourceType:
          type: string
          description: ...
          examples:
            - 'ResourceType'
        created:
          type: string
          format: date-time
          description: ...
          examples:
            - '2024-07-21T17:32:28Z'
        lastModified:
          type: string
          format: date-time
          description: ...
        location:
          type: string
          description: ...
        version:
          type: string
          description: ...
    BaseGroup:
      type: object
      description: ...
      externalDocs:
        url: 'https://datatracker.ietf.org/doc/html/rfc7643#section-4'
        description: 'RFC 7643, Section 4'
      required:
        - members
      properties:
        members:
          type: array
          description: ...
          items:
            type: object
            required:
              - value
            properties:
              value:
                type: string
                format: uuid
                description: ...
              $ref:
                type:
                  - "string"
                  - "null"
                description: ...
          default: [ ] # default is for `PUT` request
    Group:
      type: object
      description: ...
      externalDocs:
        url: 'https://datatracker.ietf.org/doc/html/rfc7643#section-4'
        description: 'RFC 7643, Section 4'
      required:
        - schemas
        - id
        - displayName
        - members
      allOf:
        - type: object
          properties:
            schemas:
              type: array
              description: ...
              items:
                type: string
            id:
              type: string
              format: uuid
              readOnly: true
              description: ...
            displayName:
              type: string
              readOnly: true
              description: ...
        - $ref: '#/components/schemas/BaseGroup'
Generation Details

Gradle configuration

openApiGenerate {
    generatorName = "kotlin-server"
    inputSpec = "${rootProject.projectDir}/rest-application/src/main/resources/test-petstore-api.yml"
    library.convention("jaxrs-spec")
    outputDir.convention("${project.layout.buildDirectory.get()}/generated-rest/server/scim")
    apiPackage.convention("com.toasttab.service.scim.api")
    modelPackage.convention("com.toasttab.service.scim.models")
    packageName.convention("com.toasttab.service.scim")
    additionalProperties.convention(
        mapOf(
            "collectionType" to "list",
            "enumPropertyNaming" to "UPPERCASE",
            "dateLibrary" to "java8",
            "interfaceOnly" to "true",
            "returnResponse" to "true"
        )
    )
}
Steps to reproduce
  1. Define a schema wit a property name of $ref
  2. Configure opernapi generator with generatorName = "kotlin-server" AND library.convention("jaxrs-spec")
  3. Attempt to generate server side code
Related issues/PRs
Suggest a fix

jasonfagerberg-toast avatar Mar 24 '25 20:03 jasonfagerberg-toast

Good Day, @jasonfagerberg-toast , Please can you confirm if you're using kotlin-server or the client version, kotlin? Also, I tried with version 7.11 on my end with kotlin and it generated a backslash as expected

data class BaseGroupMembersInner (

    /* ... */
    @get:JsonProperty("value")
    val `value`: java.util.UUID,

    /* ... */
    @get:JsonProperty("\$ref")
    val dollarRef: kotlin.String? = null

) {

DavidGrath avatar Apr 05 '25 08:04 DavidGrath

Hey @DavidGrath! Thank you for looking into than and sorry this is totally my fault, got lost when bumping around branches and gave you the wrong gradle config. Here is one that actually reproduces the error

openApiGenerate {
    generatorName = "kotlin-server"
    inputSpec = "${rootProject.projectDir}/rest-application/src/main/resources/test-petstore-api.yml"
    library.convention("jaxrs-spec")
    outputDir.convention("${project.layout.buildDirectory.get()}/generated-rest/server/scim")
    apiPackage.convention("com.toasttab.service.scim.api")
    modelPackage.convention("com.toasttab.service.scim.models")
    packageName.convention("com.toasttab.service.scim")
    additionalProperties.convention(
        mapOf(
            "serializationLibrary" to "jackson",
            "collectionType" to "list",
            "enumPropertyNaming" to "UPPERCASE",
            "dateLibrary" to "java8",
            "interfaceOnly" to "true",
            "returnResponse" to "true"
        )
    )
}

Ill update the issue body to reflect the correct config. I will put the incorrect config here for posterity

Initial Incorrect config (fixed now)
openApiGenerate {
    generatorName = "kotlin"
    inputSpec = "${rootProject.projectDir}/.toast/schemas/internal-scim-api.yaml"
    outputDir = "${getLayout().buildDirectory.get()}/generated/openapi"
    modelPackage = "com.toasttab.service.users.v3.internal.scim.models"
    additionalProperties.putAll(
        mapOf(
            "serializationLibrary" to "jackson",
            "collectionType" to "list",
            "enumPropertyNaming" to "UPPERCASE"
        )
    )
    configOptions = mapOf(
        "identifierNamingConvention" to "original"
    )
}

jasonfagerberg-toast avatar Apr 10 '25 18:04 jasonfagerberg-toast

@jasonfagerberg-toast , I see there's no Jackson in the kotlin-server generator, and so there's no $ref in an annotation, either, please can you confirm if there's still an issue with your use case?

@Serializable
data class BaseGroupMembersInner(
    /* ... */
    val `value`: java.util.UUID,
    /* ... */
    val dollarRef: kotlin.String? = null
)

DavidGrath avatar Apr 13 '25 08:04 DavidGrath

@DavidGrath so sorry for the confusion! It seems that the serializationLibrary doesn't seem to be needed at all, however jaxrs-spec server codegen implicitly uses jackson

https://github.com/OpenAPITools/openapi-generator/blob/master/modules/openapi-generator/src/main/resources/kotlin-server/libraries/jaxrs-spec/data_class.mustache#L61

jasonfagerberg-toast avatar Apr 29 '25 18:04 jasonfagerberg-toast

@jasonfagerberg-toast Thank you! So sorry I missed that, I actually didn't specify JAXRS so it defaulted to Ktor. I found out that there's a lambda called escapeDollar that seems to be for this specific issue. It was already used in the kotlin-client api files: https://github.com/OpenAPITools/openapi-generator/blob/3c664d1a59717a3ba7e56c3584b8b2648b347bbf/modules/openapi-generator/src/main/resources/kotlin-client/libraries/jvm-okhttp/api.mustache#L202 So maybe it was just overlooked Although kotlin-client by itself uses a different solution to this issue with a vendor extension: https://github.com/OpenAPITools/openapi-generator/blob/3c664d1a59717a3ba7e56c3584b8b2648b347bbf/modules/openapi-generator/src/main/resources/kotlin-client/data_class_opt_var.mustache#L12 And that vendor extension apparently does the exact same thing: https://github.com/OpenAPITools/openapi-generator/blob/3c664d1a59717a3ba7e56c3584b8b2648b347bbf/modules/openapi-generator/src/main/java/org/openapitools/codegen/languages/KotlinClientCodegen.java#L926 I'll run the necessary tests and make a PR soon

DavidGrath avatar May 01 '25 09:05 DavidGrath

@e5l Good Day, I see you're on the technical committee for Kotlin, so I wanted to add you to this discussion I found out that this issue of dollar escape not being used actually extends to other files in the kotlin-server and kotlin-client folders:

Image

For example, with kotlin-server and javalin5:

package com.toasttab.service.scim.models


/**
 *
 * @param `value` ...
 * @param dollarRef ...
 */
data class BaseGroupMembersInner(
    /* ... */

    @field:com.fasterxml.jackson.annotation.JsonProperty("value")
    val `value`: java.util.UUID,
    /* ... */

    @field:com.fasterxml.jackson.annotation.JsonProperty("$ref")
    val dollarRef: kotlin.String? = null
)

I plan to wrap all usages of baseName with the escapeDollar lambda within these folders Do you have any thoughts on this?

DavidGrath avatar May 01 '25 09:05 DavidGrath

@4brunu , please what are your thoughts?

DavidGrath avatar May 02 '25 19:05 DavidGrath

@jasonfagerberg-toast it's fixed now

DavidGrath avatar May 11 '25 18:05 DavidGrath

As far as kotlin-spring is concerned, I tried to do as thorough job as possible here: #22449

Picazsoo avatar Nov 27 '25 15:11 Picazsoo