Skip to content

Commit 289d9d1

Browse files
committed
Fixes #3263 - strip null-key entries from component schema properties before serialization
When swagger-core resolves a @JsonUnwrapped bean property (e.g. Spring HATEOAS EntityModel<T>), it uses JSON-based cloning internally. Schema.getName() is @JsonIgnore, so the name is lost during cloning. These null-named schemas are then inserted as null keys into the properties map, causing Jackson to throw JsonMappingException: Null key for a Map not allowed in JSON when serializing the OpenAPI document. Add SpringDocUtils.removeNullKeySchemas(OpenAPI) which recursively removes any null-key entry from every component schema properties map, and call it in AbstractOpenApiResource.getOpenApi() after schema resolution and before the user-facing customizers run. Five unit tests added in SpringDocUtilsTest covering top-level null keys, nested null keys, null OpenAPI, null components, and schemas with no properties.
1 parent da4cb67 commit 289d9d1

File tree

3 files changed

+137
-0
lines changed

3 files changed

+137
-0
lines changed

springdoc-openapi-starter-common/src/main/java/org/springdoc/api/AbstractOpenApiResource.java

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -407,6 +407,8 @@ protected OpenAPI getOpenApi(String serverBaseUrl, Locale locale) {
407407
if (springDocConfigProperties.isRemoveBrokenReferenceDefinitions())
408408
this.removeBrokenReferenceDefinitions(openAPI);
409409

410+
SpringDocUtils.removeNullKeySchemas(openAPI);
411+
410412
// run the optional customizers
411413
List<Server> servers = openAPI.getServers();
412414
List<Server> serversCopy = cloneViaJson(servers, new TypeReference<List<Server>>() {}, springDocProviders.jsonMapper());

springdoc-openapi-starter-common/src/main/java/org/springdoc/core/utils/SpringDocUtils.java

Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -37,6 +37,7 @@
3737
import com.fasterxml.jackson.databind.ObjectMapper;
3838
import io.swagger.v3.core.converter.AnnotatedType;
3939
import io.swagger.v3.core.util.PrimitiveType;
40+
import io.swagger.v3.oas.models.OpenAPI;
4041
import io.swagger.v3.oas.models.media.ComposedSchema;
4142
import io.swagger.v3.oas.models.media.Content;
4243
import io.swagger.v3.oas.models.media.Schema;
@@ -215,6 +216,36 @@ else if (types == null && "null".equals(addPropSchema.getType())) {
215216
}
216217
}
217218

219+
/**
220+
* Remove null-key entries from all component schema properties maps.
221+
* Guards against a swagger-core bug where @JsonUnwrapped properties can produce
222+
* null-named schemas that get inserted as null keys, causing Jackson serialization to fail.
223+
*
224+
* @param openAPI the open api
225+
*/
226+
public static void removeNullKeySchemas(OpenAPI openAPI) {
227+
if (openAPI == null || openAPI.getComponents() == null
228+
|| openAPI.getComponents().getSchemas() == null) {
229+
return;
230+
}
231+
openAPI.getComponents().getSchemas().values()
232+
.forEach(schema -> removeNullKeyFromSchemaProperties((Schema<?>) schema));
233+
}
234+
235+
/**
236+
* Recursively remove null-key entries from the given schema's properties map.
237+
*
238+
* @param schema the schema
239+
*/
240+
private static void removeNullKeyFromSchemaProperties(Schema<?> schema) {
241+
if (schema == null || schema.getProperties() == null) {
242+
return;
243+
}
244+
schema.getProperties().entrySet().removeIf(entry -> entry.getKey() == null);
245+
schema.getProperties().values()
246+
.forEach(prop -> removeNullKeyFromSchemaProperties((Schema<?>) prop));
247+
}
248+
218249
/**
219250
* Handle schema types.
220251
*
Lines changed: 104 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,104 @@
1+
/*
2+
*
3+
* *
4+
* * *
5+
* * * *
6+
* * * * * Copyright 2019-2026 the original author or authors.
7+
* * * * *
8+
* * * * * Licensed under the Apache License, Version 2.0 (the "License");
9+
* * * * * you may not use this file except in compliance with the License.
10+
* * * * * You may obtain a copy of the License at
11+
* * * * *
12+
* * * * * https://www.apache.org/licenses/LICENSE-2.0
13+
* * * * *
14+
* * * * * Unless required by applicable law or agreed to in writing, software
15+
* * * * * distributed under the License is distributed on an "AS IS" BASIS,
16+
* * * * * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
17+
* * * * * See the License for the specific language governing permissions and
18+
* * * * * limitations under the License.
19+
* * * *
20+
* * *
21+
* *
22+
*
23+
*/
24+
25+
package org.springdoc.core.utils;
26+
27+
import java.util.LinkedHashMap;
28+
import java.util.Map;
29+
30+
import io.swagger.v3.oas.models.Components;
31+
import io.swagger.v3.oas.models.OpenAPI;
32+
import io.swagger.v3.oas.models.media.Schema;
33+
import io.swagger.v3.oas.models.media.StringSchema;
34+
import org.junit.jupiter.api.Test;
35+
36+
import static org.assertj.core.api.Assertions.assertThat;
37+
38+
/**
39+
* Tests for {@link SpringDocUtils}.
40+
*/
41+
class SpringDocUtilsTest {
42+
43+
@Test
44+
void removeNullKeySchemas_removesNullKeyFromTopLevelSchemaProperties() {
45+
Schema<Object> parentSchema = new Schema<>();
46+
Map<String, Schema> props = new LinkedHashMap<>();
47+
props.put("name", new StringSchema());
48+
props.put(null, new StringSchema());
49+
props.put("count", new StringSchema());
50+
parentSchema.setProperties(props);
51+
52+
OpenAPI openAPI = new OpenAPI()
53+
.components(new Components().addSchemas("MyDto", parentSchema));
54+
55+
SpringDocUtils.removeNullKeySchemas(openAPI);
56+
57+
Map<String, Schema> result = openAPI.getComponents().getSchemas().get("MyDto").getProperties();
58+
assertThat(result).containsOnlyKeys("name", "count");
59+
assertThat(result).doesNotContainKey(null);
60+
}
61+
62+
@Test
63+
void removeNullKeySchemas_removesNullKeyFromNestedSchemaProperties() {
64+
Schema<Object> nestedSchema = new Schema<>();
65+
Map<String, Schema> nestedProps = new LinkedHashMap<>();
66+
nestedProps.put("field", new StringSchema());
67+
nestedProps.put(null, new StringSchema());
68+
nestedSchema.setProperties(nestedProps);
69+
70+
Schema<Object> parentSchema = new Schema<>();
71+
Map<String, Schema> parentProps = new LinkedHashMap<>();
72+
parentProps.put("nested", nestedSchema);
73+
parentSchema.setProperties(parentProps);
74+
75+
OpenAPI openAPI = new OpenAPI()
76+
.components(new Components().addSchemas("Parent", parentSchema));
77+
78+
SpringDocUtils.removeNullKeySchemas(openAPI);
79+
80+
Schema<?> parentResult = openAPI.getComponents().getSchemas().get("Parent");
81+
Schema<?> nestedResult = (Schema<?>) parentResult.getProperties().get("nested");
82+
assertThat(nestedResult.getProperties()).containsOnlyKeys("field");
83+
assertThat(nestedResult.getProperties()).doesNotContainKey(null);
84+
}
85+
86+
@Test
87+
void removeNullKeySchemas_toleratesNullOpenApi() {
88+
SpringDocUtils.removeNullKeySchemas(null);
89+
}
90+
91+
@Test
92+
void removeNullKeySchemas_toleratesNullComponents() {
93+
SpringDocUtils.removeNullKeySchemas(new OpenAPI());
94+
}
95+
96+
@Test
97+
void removeNullKeySchemas_toleratesSchemaWithNoProperties() {
98+
Schema<Object> emptySchema = new Schema<>();
99+
OpenAPI openAPI = new OpenAPI()
100+
.components(new Components().addSchemas("Empty", emptySchema));
101+
SpringDocUtils.removeNullKeySchemas(openAPI);
102+
assertThat(openAPI.getComponents().getSchemas().get("Empty").getProperties()).isNull();
103+
}
104+
}

0 commit comments

Comments
 (0)