Skip to content

feat: auto-set nullable: true for Kotlin nullable types in schema properties#3256

Open
thejeff77 wants to merge 2 commits intospringdoc:mainfrom
thejeff77:feat/kotlin-nullable-schema-properties
Open

feat: auto-set nullable: true for Kotlin nullable types in schema properties#3256
thejeff77 wants to merge 2 commits intospringdoc:mainfrom
thejeff77:feat/kotlin-nullable-schema-properties

Conversation

@thejeff77
Copy link
Copy Markdown

@thejeff77 thejeff77 commented Apr 7, 2026

Problem

Springdoc correctly uses Kotlin reflection (isMarkedNullable() via SpringDocKotlinUtils.kotlinNullability()) to detect nullable types for the required list — nullable fields are excluded from required in SchemaUtils.fieldRequired(). However, it does not mark the property schema itself as nullable.

This causes OpenAPI client generators (e.g., fabrikt, openapi-generator) to produce non-null types with null defaults when generating Kotlin clients, which fails compilation:

// Generated by fabrikt from a springdoc-produced spec:
data class MyModel(
    val optionalField: String = null  // ERROR: Null cannot be a value of a non-null type 'String'
)

Solution

Adds KotlinNullablePropertyCustomizer — a ModelConverter that inspects Kotlin data class properties via kotlin-reflect and marks nullable properties in the schema.

Handles both OAS versions:

OAS 3.0 (nullable: true)

Simple types:

{ "type": "string", "nullable": true }

$ref types use allOf wrapper (since $ref and nullable are mutually exclusive siblings in OAS 3.0):

{ "nullable": true, "allOf": [{ "$ref": "#/components/schemas/NestedObject" }] }

OAS 3.1 (type arrays)

Simple types:

{ "type": ["string", "null"] }

$ref types use oneOf:

{ "oneOf": [{ "$ref": "#/components/schemas/NestedObject" }, { "type": "null" }] }

The ModelConverter detects the spec version from schema.specVersion and applies the correct strategy.

Changes

File Change
KotlinNullablePropertyCustomizer.kt New — marks nullable Kotlin properties in schema, handles both OAS 3.0 and 3.1
SpringDocKotlinConfiguration.kt Registers KotlinNullablePropertyCustomizer bean
v30/app18/ OAS 3.0 test — controller with nullable fields, expected snapshot with nullable: true and allOf wrapping
v31/app23/ OAS 3.1 test — same controller, expected snapshot with type arrays and oneOf wrapping

Auto-registered in SpringDocKotlinConfiguration.KotlinReflectDependingConfiguration when kotlin-reflect is on the classpath, following the same pattern as the existing KotlinDeprecatedPropertyCustomizer.

Test

Tests verify that a controller returning a data class with nullable fields produces a spec where:

  • Non-nullable fields (requiredField: String) are in the required list and NOT marked nullable
  • Nullable primitive fields (nullableString: String?, nullableInt: Int?) are marked nullable
  • Nullable $ref fields (nullableNested: NestedObject?) are wrapped appropriately for each OAS version

Fixes #906

…perties

Springdoc correctly uses Kotlin reflection to detect nullable types
(isMarkedNullable) for the required list — nullable fields are excluded
from `required`. However, it does not set `nullable: true` on the
property schema itself. This causes OpenAPI client generators (e.g.,
fabrikt, openapi-generator) to produce non-null types with null defaults,
which fails Kotlin compilation.

Adds KotlinNullablePropertyCustomizer that inspects Kotlin data class
properties via kotlin-reflect and sets nullable: true on schema
properties whose return type is marked nullable. Auto-registered in
SpringDocKotlinConfiguration when kotlin-reflect is on the classpath.

Fixes: springdoc#906
@Mattias-Sehlstedt
Copy link
Copy Markdown
Contributor

Hi, are you sure that the client generator supports the OAS 3.0 structure

"nullableNested": {
  "nullable": true,
  "$ref": "#/components/schemas/NestedObject"
}

?

Siblings are not supported in OAS 3.0, like how in 3.1, and more often than not any definitions next to $ref is dropped. The preferred way to define siblings in 3.0 is as far as I am aware with an allOf

"nullableNested": {
  "nullable": true,
  "allOf": [
    { "$ref": "#/components/schemas/NestedObject" }
  ]
}

On the subject of OAS versions. Could you also add a test that shows the behavior for 3.1? Since nullability is expressed completely different in that specification.

@thejeff77
Copy link
Copy Markdown
Author

@bnasslahsen Hi! This PR adds automatic nullable: true support for Kotlin nullable types (Type?) on schema properties — the feature requested in #906.

The implementation follows the exact same pattern as the existing KotlinDeprecatedPropertyCustomizer (a ModelConverter registered in SpringDocKotlinConfiguration when kotlin-reflect is on the classpath).

Could you approve the CI workflow run and take a look when you get a chance? The test snapshot may need adjustment once CI runs — happy to iterate.

Addresses reviewer feedback from @Mattias-Sehlstedt:

1. $ref properties now use allOf wrapper in OAS 3.0 instead of
   sibling nullable + $ref (which is not supported per spec):
   `{ nullable: true, allOf: [{ $ref: "..." }] }`

2. OAS 3.1 nullable support added using type arrays for simple types
   (`type: ["string", "null"]`) and oneOf for $ref types
   (`oneOf: [{ $ref: "..." }, { type: "null" }]`).

3. Added v31 test (app23) with expected snapshot showing OAS 3.1
   nullable semantics alongside the existing v30 test (app18).

The ModelConverter detects the spec version from the resolved schema
and applies the appropriate nullable strategy.
@thejeff77
Copy link
Copy Markdown
Author

@Mattias-Sehlstedt Great catches — you were right on both counts. I pushed a fix addressing both:

1. $ref + nullable siblings → allOf wrapper (OAS 3.0)

"nullableNested": {
  "nullable": true,
  "allOf": [{ "$ref": "#/components/schemas/NestedObject" }]
}

Updated the v30 test snapshot (app18) accordingly.

2. OAS 3.1 support added

For simple types: "type": ["string", "null"]
For $ref types: "oneOf": [{ "$ref": "..." }, { "type": "null" }]

The ModelConverter detects the spec version via schema.specVersion and applies the appropriate strategy. Added a v31 test (app23) with the expected 3.1 nullable semantics.

We verified all of this by running our service locally and inspecting the actual generated spec — discovered the OAS 3.1 issue the hard way when schema.setNullable(true) was silently dropped during 3.1 serialization.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Support for auto conversion of Kotlin nullable type to schema: { nullable: true }

2 participants