javalin / javalin-openapi

Annotation processor for compile-time OpenAPI & JsonSchema, with out-of-the-box support for Javalin 5.x, Swagger & ReDoc
https://github.com/javalin/javalin-openapi/wiki
Apache License 2.0
45 stars 17 forks source link

Kotlin vs. OpenApi Nullable Properties: Best Practices? #220

Closed sauterl closed 3 months ago

sauterl commented 3 months ago

Disclaimer: I am not sure whether this is a bug report or a feature request, so I'm just going to ask!

As far as I am aware, nullable properties have to be specifically marked as such, with the OpenApiNullable annotation. However, since Kotlin differentiates between nullable and non-nullable types, is there a way to have the plugin automatically inject this into the OpenApi Specification?

Or did I miss another approach?

Thanks in advance!

dzikoysk commented 3 months ago

This plugin is written for Java, not Kotlin - via annotation processor API, not KSP. It means that we only have access to metadata exposed by Java's compiler and it misses most of the Kotlin's metadata - such as built-in nullability support.

The good thing, in terms of this question, is that Kotlin by default annotates these properties with @NotNull/@Nullable annotations and we have support for it:

https://github.com/javalin/javalin-openapi/blob/b2363db62d59e0851210b9c83eabe9577bf5faad/openapi-specification/src/main/kotlin/io/javalin/openapi/experimental/processor/generators/TypeSchemaGenerator.kt#L267-L274

So it should work for you out of the box. Did you notice some issues with these properties? :thinking:

sauterl commented 3 months ago

Thanks for the quick reply and the clarification regarding the target language.

I will have to investigate this on a clean slate. In our project, the generated OpenApi Specification was missing the "nullable" : true, if we didn't specify it with the @OpenApiNullable annotation. Based on your answer, this is unexpected behaviour. Unfortunately, I do not recall whether this worked as expected in a previous version of Javalin, the following affects 6.1.3 (which is a couple of minor versions behind latest, I am aware of this). version 6.1.6.

Code Example

Specifically, the following ApiClientAnswer Kotlin data class:

@Serializable
data class ApiClientAnswer( //TODO add optional relevance score field
    /** The text that is part of this [ApiClientAnswer]. */
    @get:OpenApiNullable
    val text: String? = null,

    /** The [MediaItemId] associated with the [ApiClientAnswer]. Is usually added as contextual information by the receiving endpoint. */
    @JsonIgnore
    @get:OpenApiIgnore
    val mediaItemId: MediaItemId? = null,

    /** The name of the media item that is part of the answer. */
    val mediaItemName: String?  = null,

    /** The name of the collection the media item belongs to. */
    val mediaItemCollectionName: String? = null,

    /** For temporal [ApiClientAnswer]s: Start of the segment in question in milliseconds. */
    val start: Long? = null,

    /** For temporal [ApiClientAnswer]s: End of the segment in question in milliseconds. */
    val end: Long? = null,
) {}

Produced this OpenApi schema:

"ApiClientAnswer" : {
        "type" : "object",
        "additionalProperties" : false,
        "properties" : {
          "text" : {
            "type" : "string"
          },
          "mediaItemName" : {
            "type" : "string"
          },
          "mediaItemCollectionName" : {
            "type" : "string"
          },
          "start" : {
            "type" : "integer",
            "format" : "int64"
          },
          "end" : {
            "type" : "integer",
            "format" : "int64"
          }
        }
      }

Whereas if annotated with @OpenApiNullable:

@Serializable
data class ApiClientAnswer( //TODO add optional relevance score field
    /** The text that is part of this [ApiClientAnswer]. */
    val text: String? = null,

    /** The [MediaItemId] associated with the [ApiClientAnswer]. Is usually added as contextual information by the receiving endpoint. */
    @JsonIgnore
    @get:OpenApiIgnore
    val mediaItemId: MediaItemId? = null,

    /** The name of the media item that is part of the answer. */
    @get:OpenApiNullable
    val mediaItemName: String?  = null,

    /** The name of the collection the media item belongs to. */
    @get:OpenApiNullable
    val mediaItemCollectionName: String? = null,

    /** For temporal [ApiClientAnswer]s: Start of the segment in question in milliseconds. */
    @get:OpenApiNullable
    val start: Long? = null,

    /** For temporal [ApiClientAnswer]s: End of the segment in question in milliseconds. */
    @get:OpenApiNullable
    val end: Long? = null,
) {}

the generated JSON schema is as expected:

"ApiClientAnswer" : {
        "type" : "object",
        "additionalProperties" : false,
        "properties" : {
          "text" : {
            "type" : "string",
            "nullable" : true
          },
          "mediaItemName" : {
            "type" : "string",
            "nullable" : true
          },
          "mediaItemCollectionName" : {
            "type" : "string",
            "nullable" : true
          },
          "start" : {
            "type" : "integer",
            "format" : "int64",
            "nullable" : true
          },
          "end" : {
            "type" : "integer",
            "format" : "int64",
            "nullable" : true
          }
        }
      }

EDIT: Updated the javalin dependencies in our project to 6.1.6 and reported investigation.

dzikoysk commented 3 months ago

Well, I was wrong. The Nullable/NotNull properties only affects the required list, it's unrelated to nullable property.

dzikoysk commented 3 months ago

It should work like that out of the box, you can already try it in 6.1.7-SNAPSHOT :)