Skip to content

Model properties/required missing when @JsonSerialize/@JsonDeserialize use Jackson 3 (tools.jackson) annotations #3333

Description

@denisekes

Environment

  • springdoc-openapi-starter-webmvc-ui: 3.1.0
  • swagger-core-jakarta: 2.2.52
  • Spring Boot: 4.0.7
  • Jackson: 3.x (tools.jackson.*), default for Spring Boot 4
  • Immutables: 2.12.2

Describe the bug

After migrating a Spring Boot 3 project to Spring Boot 4 (which defaults to Jackson 3), all
@JsonSerialize/@JsonDeserialize annotations were moved from com.fasterxml.jackson.databind.annotation
to tools.jackson.databind.annotation, as required for correct runtime (de)serialization under Jackson 3.

After this change, generated OpenAPI schemas for the affected classes lose all properties,
required, and nullable entries. The schema is emitted as a bare {"type":"object"}.

Root cause

org.springdoc.core.providers.ObjectMapperProvider builds its jsonMapper/yamlMapper via
io.swagger.v3.core.util.Json.mapper() / Json31.mapper(), which are backed by a classic
Jackson 2 com.fasterxml.jackson.databind.ObjectMapper. swagger-core-jakarta:2.2.52's
ModelResolver looks up @JsonSerialize/@JsonDeserialize specifically as
com.fasterxml.jackson.databind.annotation.JsonSerialize/JsonDeserialize (confirmed by
reading ModelResolver.java: the @JsonSerialize(as=...) redirect loop and property
enumeration are entirely Jackson-2-typed).

A class annotated with the Jackson 3 (tools.jackson.databind.annotation) equivalents is
therefore invisible to ModelResolver: the @JsonSerialize(as=...) redirect to the concrete
implementation class never triggers, and the abstract type's own fluent (no get/is prefix)
accessors are not auto-detected without an explicit @JsonProperty — resulting in an empty
schema.

Note @JsonProperty itself stays in com.fasterxml.jackson.annotation under Jackson 3, which is why classes with an explicit @JsonProperty on
every accessor are unaffected. Only the @JsonSerialize/@JsonDeserialize-driven redirect to
a concrete implementation type breaks.

Reproduction

// Works: properties/required resolved correctly
@JsonSerialize(as = ImmutableTest.class) // com.fasterxml.jackson.databind.annotation
@Value.Immutable
public interface Test {
    Boolean isActive();
    Optional<String> id();
}

// Broken: schema resolves to bare {"type":"object"}, no properties/required at all
@JsonDeserialize(as = ImmutableTest.class) // tools.jackson.databind.annotation
@JsonSerialize(as = ImmutableTest.class)   // tools.jackson.databind.annotation
@Value.Immutable
public interface Test {
    String isActive();
    Optional<String> id();
}

Standalone reproduction against swagger-core-jakarta:2.2.52 directly (no Spring context needed)
confirms this is not springdoc-specific glue code but the underlying ModelResolver itself:
calling ModelResolver.resolve() on both interfaces above, AvailabilityResponse resolves with
full properties, while AvailabilityRequest (identical shape, only the annotation package
differs) resolves to an empty object schema. Switching AvailabilityRequest's annotations back
to com.fasterxml.jackson.databind.annotation makes it resolve correctly.

Expected behavior

Since Spring Boot 4 / Spring Framework 7 officially support Jackson 3, and
spring.jackson.use-jackson2-defaults/com.fasterxml.jackson.databind.annotation.* classes are
being phased out in application code, springdoc's ModelResolver (or a springdoc-specific
wrapper around it) should also recognize tools.jackson.databind.annotation.JsonSerialize /
JsonDeserialize when resolving schemas, at least when the project's runtime ObjectMapper is
a Jackson 3 mapper.

Workaround

Explicitly annotate every fluent accessor with @JsonProperty
(com.fasterxml.jackson.annotation.JsonProperty, which is unaffected by the Jackson 3 package
split) instead of relying on the @JsonSerialize/@JsonDeserialize(as=...) redirect for schema
generation.

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions