Skip to content

Restore inner property name from map key in handleUnwrapped (#5126)#5193

Open
seonwooj0810 wants to merge 1 commit into
swagger-api:masterfrom
seonwooj0810:fix/issue-5126-unwrapped-null-key
Open

Restore inner property name from map key in handleUnwrapped (#5126)#5193
seonwooj0810 wants to merge 1 commit into
swagger-api:masterfrom
seonwooj0810:fix/issue-5126-unwrapped-null-key

Conversation

@seonwooj0810

Copy link
Copy Markdown

Fixes #5126

Root cause

Schema.getName() is annotated @JsonIgnore, so it is not preserved across the JSON-based clones performed inside swagger-core (AnnotationsUtils.clone, Json.mapper().readValue(Json.pretty(schema), Schema.class), etc.). The clone restores the top-level name from cloneName, but nested property schemas come back with name == null. The properties map keys are unaffected.

ModelResolver.handleUnwrapped forwards those nested schemas into the outer model's props list with props.addAll(innerModel.getProperties().values()). The subsequent for (Schema prop : props) modelProps.put(prop.getName(), prop) then inserts a null key, which causes Jackson to fail serializing the OpenAPI document with:

JsonMappingException: Null key for a Map not allowed in JSON
(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"])

In practice this is most reliably triggered by Spring HATEOAS EntityModel<T> — where getContent() is @JsonUnwrapped and the model is round-tripped through clone before unwrapping — but the underlying invariant (map key vs transient name field can drift) is general.

Change

handleUnwrapped now iterates innerModel.getProperties().entrySet() so the map key, which is the authoritative property name, can be restored on the schema when its transient name has been wiped by a clone.

The prefix/suffix branch is updated the same way: instead of building prefix + prop.getName() + suffix (which produces a literal "prefixnullsuffix" when prop.getName() is null), it falls back to the entry key when the original name has been lost.

Both branches now share the contract: a schema reached via innerModel.getProperties() is named from its map-key.

Test evidence

Ticket5126Test directly drives handleUnwrapped with an inner model whose property schemas have a null name — exactly the post-clone state described above — and asserts that the resulting property names come from the map keys, not from the cleared name field. Two cases are covered: no prefix/suffix, and prefix/suffix.

Without the change, both new tests fail:

  • noPrefixOrSuffix_restoresNameFromInnerPropertiesMapKeyEach unwrapped property must carry a non-null name
  • withPrefixAndSuffix_combinesMapKeyWhenInnerNameIsNullexpected [p_name_s, p_count_s] but got [p_null_s]

With the change, the full modules/swagger-core suite (689 tests including all existing @JsonUnwrapped and ModelResolver regression tests) passes.

Verification done

  1. gh pr list --repo swagger-api/swagger-core --search "5126" and the issue's timeline confirm no in-flight PR targets swagger-core for this bug (Fixes #3263 - strip null-key entries from component schema properties… springdoc/springdoc-openapi#3267 only adds a downstream workaround that strips null keys after swagger-core produces them; it does not fix the root cause).
  2. No "I'm working on this" comments on the issue.
  3. Fix touches .java only.
  4. Confirmed against master (6e74355ca) that the buggy props.addAll(innerModel.getProperties().values()) is still present at ModelResolver.handleUnwrapped line 1464.
  5. Standalone bug — not part of any umbrella issue I could find.
  6. The reporter's analysis includes a stack trace, reproducer (Spring HATEOAS + @JsonUnwrapped), and a fix proposal; both fix sites named there are covered.

…api#5126)

`Schema.getName()` is `@JsonIgnore`, so a JSON-based clone of a Schema
(e.g. `AnnotationsUtils.clone`) loses the name on every nested property
schema while the surrounding properties-map key still carries the correct
name. When `ModelResolver.handleUnwrapped` later forwards those property
schemas into the outer model's `props` list, the subsequent
`modelProps.put(prop.getName(), prop)` inserts a `null` key, which causes
Jackson to fail serializing the OpenAPI document with
`JsonMappingException: Null key for a Map not allowed in JSON`.

Iterate the inner properties map by entry so the map key — the
authoritative property name — can be restored on the property schema
before it is added to `props`. The prefix/suffix branch is updated the
same way: when the original prop's name has been lost by a prior clone,
fall back to the entry key instead of producing a literal
`prefix + "null" + suffix` name.

Surfaces in practice with Spring HATEOAS `EntityModel<T>` (where
`getContent()` is `@JsonUnwrapped`), but the fix is general — any path
that round-trips an inner model through `AnnotationsUtils.clone` before
unwrapping was affected.
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.

[Bug]: Null key for a Map not allowed in JSON

1 participant