diff --git a/springdoc-openapi-starter-common/src/main/java/org/springdoc/api/AbstractOpenApiResource.java b/springdoc-openapi-starter-common/src/main/java/org/springdoc/api/AbstractOpenApiResource.java index 9b6d5ffa5..e477d3993 100644 --- a/springdoc-openapi-starter-common/src/main/java/org/springdoc/api/AbstractOpenApiResource.java +++ b/springdoc-openapi-starter-common/src/main/java/org/springdoc/api/AbstractOpenApiResource.java @@ -407,6 +407,8 @@ protected OpenAPI getOpenApi(String serverBaseUrl, Locale locale) { if (springDocConfigProperties.isRemoveBrokenReferenceDefinitions()) this.removeBrokenReferenceDefinitions(openAPI); + SpringDocUtils.removeNullKeySchemas(openAPI); + // run the optional customizers List servers = openAPI.getServers(); List serversCopy = cloneViaJson(servers, new TypeReference>() {}, springDocProviders.jsonMapper()); diff --git a/springdoc-openapi-starter-common/src/main/java/org/springdoc/core/utils/SpringDocUtils.java b/springdoc-openapi-starter-common/src/main/java/org/springdoc/core/utils/SpringDocUtils.java index e705d1b09..381e35526 100644 --- a/springdoc-openapi-starter-common/src/main/java/org/springdoc/core/utils/SpringDocUtils.java +++ b/springdoc-openapi-starter-common/src/main/java/org/springdoc/core/utils/SpringDocUtils.java @@ -37,6 +37,7 @@ import com.fasterxml.jackson.databind.ObjectMapper; import io.swagger.v3.core.converter.AnnotatedType; import io.swagger.v3.core.util.PrimitiveType; +import io.swagger.v3.oas.models.OpenAPI; import io.swagger.v3.oas.models.media.ComposedSchema; import io.swagger.v3.oas.models.media.Content; import io.swagger.v3.oas.models.media.Schema; @@ -215,6 +216,36 @@ else if (types == null && "null".equals(addPropSchema.getType())) { } } + /** + * Remove null-key entries from all component schema properties maps. + * Guards against a swagger-core bug where @JsonUnwrapped properties can produce + * null-named schemas that get inserted as null keys, causing Jackson serialization to fail. + * + * @param openAPI the open api + */ + public static void removeNullKeySchemas(OpenAPI openAPI) { + if (openAPI == null || openAPI.getComponents() == null + || openAPI.getComponents().getSchemas() == null) { + return; + } + openAPI.getComponents().getSchemas().values() + .forEach(schema -> removeNullKeyFromSchemaProperties((Schema) schema)); + } + + /** + * Recursively remove null-key entries from the given schema's properties map. + * + * @param schema the schema + */ + private static void removeNullKeyFromSchemaProperties(Schema schema) { + if (schema == null || schema.getProperties() == null) { + return; + } + schema.getProperties().entrySet().removeIf(entry -> entry.getKey() == null); + schema.getProperties().values() + .forEach(prop -> removeNullKeyFromSchemaProperties((Schema) prop)); + } + /** * Handle schema types. * diff --git a/springdoc-openapi-starter-common/src/test/java/org/springdoc/core/utils/SpringDocUtilsTest.java b/springdoc-openapi-starter-common/src/test/java/org/springdoc/core/utils/SpringDocUtilsTest.java new file mode 100644 index 000000000..02bd1a51f --- /dev/null +++ b/springdoc-openapi-starter-common/src/test/java/org/springdoc/core/utils/SpringDocUtilsTest.java @@ -0,0 +1,104 @@ +/* + * + * * + * * * + * * * * + * * * * * Copyright 2019-2026 the original author or authors. + * * * * * + * * * * * Licensed under the Apache License, Version 2.0 (the "License"); + * * * * * you may not use this file except in compliance with the License. + * * * * * You may obtain a copy of the License at + * * * * * + * * * * * https://www.apache.org/licenses/LICENSE-2.0 + * * * * * + * * * * * Unless required by applicable law or agreed to in writing, software + * * * * * distributed under the License is distributed on an "AS IS" BASIS, + * * * * * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * * * * * See the License for the specific language governing permissions and + * * * * * limitations under the License. + * * * * + * * * + * * + * + */ + +package org.springdoc.core.utils; + +import java.util.LinkedHashMap; +import java.util.Map; + +import io.swagger.v3.oas.models.Components; +import io.swagger.v3.oas.models.OpenAPI; +import io.swagger.v3.oas.models.media.Schema; +import io.swagger.v3.oas.models.media.StringSchema; +import org.junit.jupiter.api.Test; + +import static org.assertj.core.api.Assertions.assertThat; + +/** + * Tests for {@link SpringDocUtils}. + */ +class SpringDocUtilsTest { + + @Test + void removeNullKeySchemas_removesNullKeyFromTopLevelSchemaProperties() { + Schema parentSchema = new Schema<>(); + Map props = new LinkedHashMap<>(); + props.put("name", new StringSchema()); + props.put(null, new StringSchema()); + props.put("count", new StringSchema()); + parentSchema.setProperties(props); + + OpenAPI openAPI = new OpenAPI() + .components(new Components().addSchemas("MyDto", parentSchema)); + + SpringDocUtils.removeNullKeySchemas(openAPI); + + Map result = openAPI.getComponents().getSchemas().get("MyDto").getProperties(); + assertThat(result).containsOnlyKeys("name", "count"); + assertThat(result).doesNotContainKey(null); + } + + @Test + void removeNullKeySchemas_removesNullKeyFromNestedSchemaProperties() { + Schema nestedSchema = new Schema<>(); + Map nestedProps = new LinkedHashMap<>(); + nestedProps.put("field", new StringSchema()); + nestedProps.put(null, new StringSchema()); + nestedSchema.setProperties(nestedProps); + + Schema parentSchema = new Schema<>(); + Map parentProps = new LinkedHashMap<>(); + parentProps.put("nested", nestedSchema); + parentSchema.setProperties(parentProps); + + OpenAPI openAPI = new OpenAPI() + .components(new Components().addSchemas("Parent", parentSchema)); + + SpringDocUtils.removeNullKeySchemas(openAPI); + + Schema parentResult = openAPI.getComponents().getSchemas().get("Parent"); + Schema nestedResult = (Schema) parentResult.getProperties().get("nested"); + assertThat(nestedResult.getProperties()).containsOnlyKeys("field"); + assertThat(nestedResult.getProperties()).doesNotContainKey(null); + } + + @Test + void removeNullKeySchemas_toleratesNullOpenApi() { + SpringDocUtils.removeNullKeySchemas(null); + } + + @Test + void removeNullKeySchemas_toleratesNullComponents() { + SpringDocUtils.removeNullKeySchemas(new OpenAPI()); + } + + @Test + void removeNullKeySchemas_toleratesSchemaWithNoProperties() { + Schema emptySchema = new Schema<>(); + OpenAPI openAPI = new OpenAPI() + .components(new Components().addSchemas("Empty", emptySchema)); + SpringDocUtils.removeNullKeySchemas(openAPI); + assertThat(openAPI.getComponents().getSchemas().get("Empty").getProperties()).isNull(); + } +}