feat: auto-set nullable: true for Kotlin nullable types in schema properties#3256
Conversation
…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
|
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 "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. |
|
@bnasslahsen Hi! This PR adds automatic The implementation follows the exact same pattern as the existing 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.
|
@Mattias-Sehlstedt Great catches — you were right on both counts. I pushed a fix addressing both: 1. "nullableNested": {
"nullable": true,
"allOf": [{ "$ref": "#/components/schemas/NestedObject" }]
}Updated the v30 test snapshot (app18) accordingly. 2. OAS 3.1 support added For simple types: The 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 |
|
Additional finding from local testing: This PR fixes the |
…roperties feat: auto-set nullable: true for Kotlin nullable types in schema properties
|
This completely broke my frontend in typescript. Now open api generator created value?: string | null in stead of before value?:string. How do I disable this globally? |
|
Now {
"type": [
"integer",
"null"
],
"format": "int64"
}Vs before: {
"type": "integer",
"format": "int64"
}EDIT: removed the question after relearning how to read. |
|
Ok, now my typescript frontend ist also broken. @0xabadea How can I disable this behavior...? |
|
You can remove |
|
@0xabadea |
|
As far as typescript, this is only loosely related. This fixes a bug where nullable kotlin types are incorrectly mapped to non-nullable Openapi types. This will potentially adversely impact typescript generators or other generators based on the generated spec from kotlin API definitions - in that what it generates will now correctly become optional or nullable. The previous openapi spec was incorrect. Previously clients would have crashed if a null was correctly returned from the kotlin API and the clients were hard-coded to handle non-nullable types. Now clients that depend on the new artifact would need to update some minimal code to correctly handle the nullable type. Is it better to have a potential crash? Or is it better to fix a minor compilation error on the newly generated client? This depends on perspective and use case.. generally, a crash could be considered worse. The minor null handling tweaks on client libraries is acceptable in my opinion.. The spec is now correctly generated, which will impact client generation by requiring some small changes, but that is a downstream side-effect, not a breakage in the springdoc library itself. Regarding the customizer, I can't confirm if this is needed after startup or not, but I'd be open to other ways of doing this! |
|
Hi @thejeff77, I think there is some more nuance there. I would encourage you to read my rationale in #3269. The previous spec was not necessarily incorrect. If the Kotlin API documents to never return nulls, just undefined-s, and it never returns nulls, then the spec is correct and there is no crash. I do understand that your original use case was different. Regarding whether it's safe to remove the customizer from |
|
@0xabadea — I went back through this with fresh eyes and pulled the swagger-core history. You were right on the substance, and I want to lay out where I've landed. On the concern itself — it's valid and I underweighted it earlier. This isn't really a TypeScript-generator complaint. The actual problem is that my PR introduced auto-detection of nullable from Kotlin Why I'm pushing for reverting my PR rather than landing #3269 as the fix. I want to be direct about this because the reasoning matters: The deeper bug isn't "auto-detection is too aggressive globally." It's that users have no way to override the auto-detection per-property via the natural A global flag (#3269) addresses the symptom but leaves the per-property bug in place. A codebase that mixes Jackson configs — some DTOs Reverting cleanly removes the bug. The The proper fix lives upstream in swagger-core, not in springdoc. This Mode pattern is already established there, twice, deliberately:
Swagger-core has also been independently building auto-detection of nullable on the same rail: swagger-api/swagger-core#5018 (2025-11) added native What I've done about it:
Thanks for the patience while I caught up. |
|
Update: the upstream
Once that lands and ships, springdoc can re-introduce this customizer with |
|
@thejeff77 very much in agreement with everything that you've said. Thank you for the revert, that is indeed the better way going forward. There's one thing I want to touch on. You mention in a number of places that the In practice, we use Of course, the ability to override the global nullable mode on a per-property basis remains beneficial and perfectly in line with the ability to override the Spring-configured Jackson inclusion via |
Problem
Springdoc correctly uses Kotlin reflection (
isMarkedNullable()viaSpringDocKotlinUtils.kotlinNullability()) to detect nullable types for the required list — nullable fields are excluded fromrequiredinSchemaUtils.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:
Solution
Adds
KotlinNullablePropertyCustomizer— aModelConverterthat inspects Kotlin data class properties viakotlin-reflectand marks nullable properties in the schema.Handles both OAS versions:
OAS 3.0 (
nullable: true)Simple types:
{ "type": "string", "nullable": true }$reftypes useallOfwrapper (since$refandnullableare mutually exclusive siblings in OAS 3.0):{ "nullable": true, "allOf": [{ "$ref": "#/components/schemas/NestedObject" }] }OAS 3.1 (
typearrays)Simple types:
{ "type": ["string", "null"] }$reftypes useoneOf:{ "oneOf": [{ "$ref": "#/components/schemas/NestedObject" }, { "type": "null" }] }The
ModelConverterdetects the spec version fromschema.specVersionand applies the correct strategy.Changes
KotlinNullablePropertyCustomizer.ktSpringDocKotlinConfiguration.ktKotlinNullablePropertyCustomizerbeanv30/app18/nullable: trueandallOfwrappingv31/app23/typearrays andoneOfwrappingAuto-registered in
SpringDocKotlinConfiguration.KotlinReflectDependingConfigurationwhenkotlin-reflectis on the classpath, following the same pattern as the existingKotlinDeprecatedPropertyCustomizer.Test
Tests verify that a controller returning a data class with nullable fields produces a spec where:
requiredField: String) are in therequiredlist and NOT marked nullablenullableString: String?,nullableInt: Int?) are marked nullable$reffields (nullableNested: NestedObject?) are wrapped appropriately for each OAS versionFixes #906