kotlinopenapiopenapi-generator

open-api-generator how to avoid fields being nullable


When I use the open-api-generator all of the fields are nullable, all classes look like this:

data class PagedResultDtoOfStoredDeviceFilterWithoutTermDto (

    @Json(name = "currentPage")
    val currentPage: kotlin.Int? = null,

    @Json(name = "totalPages")
    val totalPages: kotlin.Int? = null,

    @Json(name = "pageSize")
    val pageSize: kotlin.Int? = null,

    @Json(name = "totalCount")
    val totalCount: kotlin.Int? = null,

    @Json(name = "items")
    val items: kotlin.collections.List<StoredDeviceFilterWithoutTermDto>? = null

)

Despite many fields are NOT nullable server-side, as you can see in the swagger.json This makes the generator unusable to me, do somebody know why this happens?

This is my generator config:

java -jar openapi-generator-cli.jar generate ^
     -g kotlin ^
     --library jvm-retrofit2 ^
     -i "https://app-hsm-clse-stage.azurewebsites.net/swagger/standardV1/swagger.json" ^
     -p groupId=com.mycompany ^
     -p artifactId=mycompany-myproduct-client-kotlin ^
     -p artifactVersion=1.0.0 ^
     -p basePackage=com.mycompany.myproduct.networking ^
     -p packageName=com.mycompany.myproduct.networking ^
     -p configPackage=com.mycompany.myproduct.networking.config ^
     -p apiPackage=com.mycompany.myproduct.networking.api ^
     -p modelPackage=com.mycompany.myproduct.networking.model ^
     -p sourceFolder=src/main/java ^
     -p dateLibrary=java8 ^
     -p java8=true ^
     -p useRxJava3=true ^
     
pause

Solution

  • You could potentially accomplish this using templates and extensions.

    Create a vendor extension called x-is-nullable and add it to your object schema. I'm using your AbsoluteDateIntervalDto object as an example:

        AbsoluteDateIntervalDto:
          type: object
          properties:
            startDate:
              type: string
              format: date-time
              x-is-nullable: "false"
            endDate:
              type: string
              format: date-time
              x-is-nullable: "false"
            startIncluded:
              type: boolean
              default: true
            endIncluded:
              type: boolean
          additionalProperties: false
    

    Note that I added a default value to startIncluded to demonstrate how this works with default values as well.

    now, change the template for data_class_opt_var.mustache. Replace the last line so that it looks like this:

    {{^vendorExtensions.x-is-nullable}}{{#multiplatform}}@SerialName(value = "{{{vendorExtensions.x-base-name-literal}}}") {{/multiplatform}}{{#isInherited}}override {{/isInherited}}{{>modelMutable}} {{{name}}}: {{#isArray}}{{#isList}}{{#uniqueItems}}kotlin.collections.{{#modelMutable}}Mutable{{/modelMutable}}Set{{/uniqueItems}}{{^uniqueItems}}kotlin.collections.{{#modelMutable}}Mutable{{/modelMutable}}List{{/uniqueItems}}{{/isList}}{{^isList}}kotlin.Array{{/isList}}<{{^items.isEnum}}{{^items.isPrimitiveType}}{{^items.isModel}}{{#kotlinx_serialization}}@Contextual {{/kotlinx_serialization}}{{/items.isModel}}{{/items.isPrimitiveType}}{{{items.dataType}}}{{/items.isEnum}}{{#items.isEnum}}{{classname}}.{{{nameInCamelCase}}}{{/items.isEnum}}>{{/isArray}}{{^isEnum}}{{^isArray}}{{{dataType}}}{{/isArray}}{{/isEnum}}{{#isEnum}}{{^isArray}}{{classname}}.{{{nameInCamelCase}}}{{/isArray}}{{/isEnum}}? = {{^defaultValue}}null{{/defaultValue}}{{#defaultValue}}{{^isNumber}}{{{defaultValue}}}{{/isNumber}}{{#isNumber}}{{^multiplatform}}{{{dataType}}}("{{{defaultValue}}}"){{/multiplatform}}{{#multiplatform}}({{{defaultValue}}}).toDouble(){{/multiplatform}}{{/isNumber}}{{/defaultValue}}{{/vendorExtensions.x-is-nullable}}{{#vendorExtensions.x-is-nullable}}{{#multiplatform}}@SerialName(value = "{{{vendorExtensions.x-base-name-literal}}}") {{/multiplatform}}{{#isInherited}}override {{/isInherited}}{{>modelMutable}} {{{name}}}: {{#isArray}}{{#isList}}{{#uniqueItems}}kotlin.collections.{{#modelMutable}}Mutable{{/modelMutable}}Set{{/uniqueItems}}{{^uniqueItems}}kotlin.collections.{{#modelMutable}}Mutable{{/modelMutable}}List{{/uniqueItems}}{{/isList}}{{^isList}}kotlin.Array{{/isList}}<{{^items.isEnum}}{{^items.isPrimitiveType}}{{^items.isModel}}{{#kotlinx_serialization}}@Contextual {{/kotlinx_serialization}}{{/items.isModel}}{{/items.isPrimitiveType}}{{{items.dataType}}}{{/items.isEnum}}{{#items.isEnum}}{{classname}}.{{{nameInCamelCase}}}{{/items.isEnum}}>{{/isArray}}{{^isEnum}}{{^isArray}}{{{dataType}}}{{/isArray}}{{/isEnum}}{{#isEnum}}{{^isArray}}{{classname}}.{{{nameInCamelCase}}}{{/isArray}}{{/isEnum}}{{#defaultValue}} = {{^isNumber}}{{{defaultValue}}}{{/isNumber}}{{#isNumber}}{{^multiplatform}}{{{dataType}}}("{{{defaultValue}}}"){{/multiplatform}}{{#multiplatform}}({{{defaultValue}}}).toDouble(){{/multiplatform}}{{/isNumber}}{{/defaultValue}}{{/vendorExtensions.x-is-nullable}}
    

    Note: this is a very long line, however it must all be on one line for it to work correctly.

    Here's how it works. The open-api generator will put all vendor extensions into an array called vendorExtensions that is accessible to the mustache templating engine. The line {{^vendorExtensions.x-is-nullable}} checks to see if the x-is-nullable extension exists in the array. the carrot(^) is basically equals to if (x-is-nullable.doesNotExist()). The value false assigned in the preceding yaml is arbitrary. You can have any value you want there, but a value must exist.

    Now the template will execute all of the code between the {{^vendorExtensions.x-is-nullable}} and the closing {{/vendorExtensions.x-is-nullable}} if the vendor extension doesn't exist, and it will execute all the code between {{#vendorExtensions.x-is-nullable}}{{/vendorExtensions.x-is-nullable}} if it does. This allows two different generations to occur in the same class.

    Your new resulting data class from the above template change, using the above schema, will now look like this

    data class AbsoluteDateIntervalDto (
    
        @Json(name = "startDate")
        val startDate: java.time.OffsetDateTime,
    
        @Json(name = "endDate")
        val endDate: java.time.OffsetDateTime,
    
        @Json(name = "startIncluded")
        val startIncluded: kotlin.Boolean = true,
    
        @Json(name = "endIncluded")
        val endIncluded: kotlin.Boolean? = null
    
    )