swagger-api / swagger-api/swagger-core
[Bug]: Null key for a Map not allowed in JSON
Nobody has claimed this yet.
- Dominant language
- Java
- Stars
- 7.5k
- Forks
- 2.3k
- Avg merge
- 18h 1m
- Merged PRs (30d)
- 10
Description
Description of the problem/issue
When swagger-core resolves a bean property annotated with @JsonUnwrapped, the generated OpenAPI schema's properties map may contain a null key entry, which causes Jackson serialization to fail with JsonMappingException: Null key for a Map not allowed in JSON.
This is triggered by Spring HATEOAS EntityModel<T>, where getContent() is annotated with @JsonUnwrapped. When swagger-core resolves EntityModel<SomeDto>, it unwraps the DTO's properties into the parent schema. However, the unwrapped property Schema objects may have name == null due to Schema.getName() being @JsonIgnore and being lost during JSON-based clone operations within ModelResolver. These null-named schemas are then inserted into the properties map via modelProps.put(prop.getName(), prop), producing a null key.
Root Cause Analysis
The bug is in ModelResolver.java and involves three interacting mechanisms:
1. handleUnwrapped() (line ~1461) extracts Schema values from the inner model's properties map:
private void handleUnwrapped(List<Schema> props, Schema innerModel,
String prefix, String suffix, List<String> requiredProps) {
if (StringUtils.isBlank(suffix) && StringUtils.isBlank(prefix)) {
if (innerModel.getProperties() != null) {
props.addAll(innerModel.getProperties().values()); // Schema values added directly
// ...
}
}
// ...
}
2. Schema.getName() is @JsonIgnore, lost during any JSON round-trip clone:
// io.swagger.v3.oas.models.media.Schema
@JsonIgnore
public String getName() {
return this.name;
}
The AnnotationsUtils.clone() method only restores the name for the top-level schema, not for nested property schemas:
public static Schema clone(Schema schema, boolean openapi31) {
String cloneName = schema.getName();
schema = Json.mapper().readValue(Json.pretty(schema), Schema.class); // name lost for nested schemas
schema.setName(cloneName); // only top-level name is restored
return schema;
}
3. Null key insertion (line ~940):
for (Schema prop : props) {
modelProps.put(prop.getName(), prop); // null key if prop.getName() is null
}
When handleUnwrapped retrieves property schemas from a model that has been through a clone/re-resolution cycle (e.g., $ref dereferencing at line ~817, resolveSubtypes, or context.defineModel + later getDefinedModels retrieval), the nested property Schema objects have name == null. These are added to the props list by handleUnwrapped, and then modelProps.put(prop.getName(), prop) inserts a null key into the LinkedHashMap.
Suggested Fix
In handleUnwrapped(), when no prefix/suffix is specified, preserve the property name from the inner model's properties map key instead of relying on Schema.getName():
private void handleUnwrapped(List<Schema> props, Schema innerModel,
String prefix, String suffix, List<String> requiredProps) {
if (StringUtils.isBlank(suffix) && StringUtils.isBlank(prefix)) {
if (innerModel.getProperties() != null) {
for (Map.Entry<String, Schema> entry : innerModel.getProperties().entrySet()) {
Schema prop = entry.getValue();
if (prop.getName() == null) {
prop.setName(entry.getKey()); // restore name from map key
}
props.add(prop);
}
if (innerModel.getRequired() != null) {
requiredProps.addAll(innerModel.getRequired());
}
}
}
// ...
}
Alternatively, the null key insertion point at line ~940 could be guarded:
for (Schema prop : props) {
if (prop.getName() != null) {
modelProps.put(prop.getName(), prop);
}
}
Affected Version
- swagger-core-jakarta: 2.2.47
- swagger-models-jakarta: 2.2.47
Earliest version the bug appears in (if known): likely any version that includes the handleUnwrapped method using props.addAll(innerModel.getProperties().values()) combined with @JsonIgnore on Schema.getName().
Steps to Reproduce
-
Add swagger-core-jakarta 2.2.47 (e.g., via springdoc-openapi-starter-webmvc-ui 3.0.3) to a Spring Boot project that also uses Spring HATEOAS.
-
Define a DTO:
public record MyDto(String name, int count) {}
- Define a REST endpoint returning
EntityModel<MyDto>:
@GetMapping("/items")
public PagedModel<EntityModel<MyDto>> getItems(PagedResourcesAssembler<MyDto> assembler, Pageable pageable) {
return assembler.toModel(repository.findAll(pageable));
}
Spring HATEOAS EntityModel.getContent() is annotated with @JsonUnwrapped, which triggers the unwrapped schema resolution path in ModelResolver.
-
Access the OpenAPI docs endpoint:
GET /v3/api-docs -
The request fails with a
JsonMappingExceptionbecause the generated schema forEntityModelMyDtohas anullkey in itspropertiesmap.
Expected Behavior
The OpenAPI schema for EntityModelMyDto should contain properly keyed properties (e.g., name, count, _links) with no null entries in the properties map.
Actual Behavior
The properties map of the EntityModelMyDto schema contains a null key, causing Jackson to fail during serialization of the OpenAPI spec:
JsonMappingException: Null key for a Map not allowed in JSON (use a converting NullKeySerializer?)
(through reference chain:
io.swagger.v3.oas.models.OpenAPI["components"]
->io.swagger.v3.oas.models.Components["schemas"]
->java.util.LinkedHashMap["EntityModelMyDto"]
->io.swagger.v3.oas.models.media.JsonSchema["properties"]
->java.util.LinkedHashMap["null"])
Logs / Stack Traces
com.fasterxml.jackson.databind.JsonMappingException: Null key for a Map not allowed in JSON (use a converting NullKeySerializer?) (through reference chain: io.swagger.v3.oas.models.OpenAPI["components"]->io.swagger.v3.oas.models.Components["schemas"]->java.util.LinkedHashMap["EntityModelContractResponse"]->io.swagger.v3.oas.models.media.JsonSchema["properties"]->java.util.LinkedHashMap["null"])
at com.fasterxml.jackson.databind.JsonMappingException.from(JsonMappingException.java:282)
at com.fasterxml.jackson.databind.SerializerProvider.mappingException(SerializerProvider.java:1417)
at com.fasterxml.jackson.databind.SerializerProvider.reportMappingProblem(SerializerProvider.java:1315)
at com.fasterxml.jackson.databind.ser.impl.FailingSerializer.serialize(FailingSerializer.java:31)
at com.fasterxml.jackson.databind.ser.std.MapSerializer.serializeOptionalFields(MapSerializer.java:867)
at com.fasterxml.jackson.databind.ser.std.MapSerializer.serializeWithoutTypeInfo(MapSerializer.java:759)
at com.fasterxml.jackson.databind.ser.std.MapSerializer.serialize(MapSerializer.java:719)
at com.fasterxml.jackson.databind.ser.std.MapSerializer.serialize(MapSerializer.java:34)
at com.fasterxml.jackson.databind.ser.BeanPropertyWriter.serializeAsField(BeanPropertyWriter.java:732)
at com.fasterxml.jackson.databind.ser.std.BeanSerializerBase.serializeFields(BeanSerializerBase.java:760)
at com.fasterxml.jackson.databind.ser.BeanSerializer.serialize(BeanSerializer.java:183)
at io.swagger.v3.core.jackson.Schema31Serializer.serialize(Schema31Serializer.java:51)
at io.swagger.v3.core.jackson.Schema31Serializer.serialize(Schema31Serializer.java:12)
at com.fasterxml.jackson.databind.ser.std.MapSerializer.serializeOptionalFields(MapSerializer.java:868)
at com.fasterxml.jackson.databind.ser.std.MapSerializer.serializeWithoutTypeInfo(MapSerializer.java:759)
at com.fasterxml.jackson.databind.ser.std.MapSerializer.serialize(MapSerializer.java:719)
at com.fasterxml.jackson.databind.ser.std.MapSerializer.serialize(MapSerializer.java:34)
Additional Context
Environment
- Spring Boot 4.0.5
- Spring HATEOAS 3.0.3
- springdoc-openapi-starter-webmvc-ui 3.0.3
- swagger-core-jakarta 2.2.47
- Kotlin 2.3.20
Workaround
Register a springdoc OpenApiCustomizer bean to strip null keys from all schema properties maps after generation:
@Bean
public OpenApiCustomizer nullKeyCleanupCustomizer() {
return openApi -> {
if (openApi.getComponents() != null && openApi.getComponents().getSchemas() != null) {
openApi.getComponents().getSchemas().values().forEach(this::removeNullKeys);
}
};
}
private void removeNullKeys(Schema<?> schema) {
if (schema.getProperties() != null) {
schema.getProperties().entrySet().removeIf(e -> e.getKey() == null);
schema.getProperties().values().forEach(this::removeNullKeys);
}
}
Key code references in swagger-core 2.2.47
| File | Line(s) | Description |
|---|---|---|
ModelResolver.java |
~796-812 | jsonUnwrappedHandler lambda registration |
ModelResolver.java |
~940-941 | modelProps.put(prop.getName(), prop) — null key insertion point |
ModelResolver.java |
~1461-1489 | handleUnwrapped() — extracts property schemas relying on Schema.getName() |
Schema.java |
getName() |
@JsonIgnore annotation causes name loss during JSON round-trip clone |
AnnotationsUtils.java |
clone() |
Only restores top-level schema name, not nested property names |
Checklist
- I have searched the existing issues and this is not a duplicate.
- I have provided sufficient information for maintainers to reproduce the issue.
Contributor guide
First steps
- Read the whole issue, then the project's contributing guide.
- Comment on the issue to say you are picking it up — it saves two people doing the same work.
- Fork the repository and make your change on a branch.
- Open a pull request that references the issue number.
Research direction
Start in ModelResolver.java at handleUnwrapped() and the modelProps.put(prop.getName(), prop) insertion point, then inspect Schema.java and AnnotationsUtils.java clone() for how names are preserved. Reproduce through the GET /v3/api-docs endpoint using the EntityModel example. Done means the generated EntityModel schema has keyed properties and serializes without a null-key JsonMappingException.
Written by the indexing model from the issue text.
Assessment
- Tech stack
- java, spring-boot
- Domain
- api, backend-api-design
- Issue type
- Bug
- Difficulty
- 3/5
- Estimated time
- 1-2 days
- Activity status
- Quiet
- Clarity
- Clearly specified
- Newbie friendliness
- 58/100