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.82k stars 6.58k forks source link

[BUG] [Kotlin] [Gradle Plugin] [Android] Generated enum classes expect strings when specification specifies integers #8458

Open stephen-mojo opened 3 years ago

stephen-mojo commented 3 years ago
Description

I am using the OpenApiGenerator via the Gradle plugin in my Kotlin Android project. I am running into an issue where generated enum classes are expecting strings even though the specification specifies integers. The means Moshi is unable to parse enum values in my sever responses.

Enums in my OpenAPI specification file are of this form:

"MediaType": {
    "enum": [
        0,
        1,
        2,
        3
    ],
    "type": "integer",
    "format": "int32",
    "x-enum-varnames": [
        "unknown",
        "video",
        "audio",
        "text"
    ]
}

The resulting generated enums are of this form:

enum class MediaType(val value: kotlin.Int) {

    @Json(name = "0")
    unknown(0),

    @Json(name = "1")
    video(1),

    @Json(name = "2")
    audio(2),

    @Json(name = "3")
    text(3);

    ...
}

This leads to unsuccessful parsing of enums by Moshi. The resulting exception is of this form:

com.squareup.moshi.JsonDataException: Expected one of [0, 1, 2, 3] but was 2 at path $[0].mediaType
      at com.squareup.moshi.StandardJsonAdapters$EnumJsonAdapter.fromJson(StandardJsonAdapters.java:297)
      at com.squareup.moshi.StandardJsonAdapters$EnumJsonAdapter.fromJson(StandardJsonAdapters.java:264)
      at com.squareup.moshi.internal.NullSafeJsonAdapter.fromJson(NullSafeJsonAdapter.java:41)
      ...
openapi-generator version

classpath "org.openapitools:openapi-generator-gradle-plugin:5.0.0"

OpenAPI declaration file content or url

I am using the following configuration for the Gradle plugin:

openApiGenerate {
    generatorName = "kotlin"
    inputSpec = "$projectDir/openapi-spec.json"
    ignoreFileOverride = "$projectDir/.openapi-generator-ignore"
    outputDir = "$buildDir/generated/openapi"
    apiPackage = "com.example.api.service"
    modelPackage = "com.example.api.model"
    configFile = "$projectDir/openapi-generator-config.json"
}

This is the contents of 'openapi-generator-config.json':

{
  "apiSuffix": "Service",
  "collectionType": "list",
  "enumPropertyNaming": "UPPERCASE",
  "library": "jvm-retrofit2",
  "modelMutable": "false",
  "moshiCodeGen": "true",
  "packageName": "com.example.api",
  "parcelizeModels": "false",
  "serializableModel": "false",
  "sortModelPropertiesByRequiredFlag": "true",
  "sortParamsByRequiredFlag": "true",
  "useCoroutines": "true"
}
Suggest a fix

I am considering writing custom adapters somewhere to handle this manually, but it would be very tedious to have to do this for every single enum type in my spec. Is there an easier solution out there? Unfortunately, I don't have much control over the form the enums are in the specification. For reasons on the back end, I think I am stuck with x-enum-varnames.

auto-labeler[bot] commented 3 years ago

👍 Thanks for opening this issue! 🏷 I have applied any labels matching special text in your issue.

The team will review the labels and make any necessary changes.

stephen-mojo commented 3 years ago

I added this VERY quick and dirty Moshi adapter for the time being and added an instance of it for each of my enums. It is probably missing a lot of error handling and I am not really sure if I am doing things correctly as I have never worked with reflecting annotations before, but it seems to work:

class IntegerEnumAdapter<T : Enum<T>>(enumType: Class<T>): JsonAdapter<T>() {
    private val intToEnumMap = mutableMapOf<Int, T>()
    private val enumToIntMap = mutableMapOf<T, Int>()

    init {
        // Loop through all of the possible enum values.
        enumType.enumConstants.forEach { enum ->
            // For each possible enum, get the associated name specified in its annotation.
            val constantName: String = enum.name
            val annotation = enumType.getField(constantName).getAnnotation(Json::class.java)

            // The name is always an integer in string form. It is safe to convert it to an integer.
            val integerEnumNameAnnotation = annotation.name.toInt()

            intToEnumMap[integerEnumNameAnnotation] = enum
            enumToIntMap[enum] = integerEnumNameAnnotation
        }
    }

    override fun fromJson(reader: JsonReader): T? {
        val integer = reader.nextInt()
        val enum = intToEnumMap[integer]
        return enum
    }

    override fun toJson(writer: JsonWriter, enum: T?) {
        val integer = enumToIntMap[enum]
        writer.value(integer)
    }
}
ravermeister commented 3 years ago

sounds very similar to #8442

ravermeister commented 3 years ago

The Bug in #8442 seems fixed now :)