Skip to content

feat: API to add Jackson modules / customize immutable Jackson 3 mappers - #5324

Merged
daniel-kmiecik merged 5 commits into
swagger-api:3.0.0from
pjfanning:jackson3-mapper-customizers
Sep 25, 2026
Merged

daniel-kmiecik merged 5 commits into
swagger-api:3.0.0from
pjfanning:jackson3-mapper-customizers

Conversation

@pjfanning

Copy link
Copy Markdown
Contributor

Targets the 3.0.0 branch (Jackson 3).

Problem

Jackson 3 ObjectMappers are immutable. With Jackson 2, the documented way to make swagger-core aware of Kotlin/Scala/Jakarta XML Bind/Guava/… types was

Json.mapper().registerModule(new KotlinModule.Builder().build());

and it worked for two reasons: Json.mapper() was mutable, and AbstractModelConverter kept the same instance, so the default ModelResolver in ModelConverters saw the module too (that is what drives model introspection; see the _intr() comment about users loading modules later).

On 3.0.0 both are gone: Json/Yaml/Json31/Yaml31 hold a static final mapper with no way to replace it, and AbstractModelConverter now rebuild()s a private copy, so even a swapped Json.mapper() would not reach an already-created resolver. The only surviving hook is ObjectMapperProcessor, which is scoped to an OpenApiContext and appends an extra resolver rather than configuring the ~80 internal Json.mapper() call sites (ModelDeserializer, AnnotationsUtils, SwaggerSerializers, plugins, …).

This is the Jackson 3 version of what I proposed in #4052 / draft PR #4053 (Jackson 2, builder-based ObjectMapperFactory with a user-modifiable configuration and reset). Related: #3837 (findAndRegisterModules request), #4991 (Jackson 3 support), #5314 (the Jackson 3 migration this builds on).

Changes

ObjectMapperFactory customizer registry (new, swagger-core)

ObjectMapperFactory.addModule(new KotlinModule.Builder().build());      // every mapper swagger-core builds
ObjectMapperFactory.addCustomizer((builder, target) -> {                 // full MapperBuilder access
    if (target.isYaml()) { builder.enable(SerializationFeature.INDENT_OUTPUT); }
});
  • New MapperCustomizer ((MapperBuilder<?,?>, MapperTarget) -> void) and MapperTarget enum (JSON, YAML, JSON31, YAML31, JSON_CONVERTER).
  • addCustomizer / removeCustomizer / clearCustomizers / getCustomizers, addModule(s) shortcuts, and a generation() counter.
  • Customizers run as the last step of create(...) and createJsonConverter(), after swagger-core's modules/mixins/inclusion settings, so user config wins on conflicts — same precedence registerModule-after-construction had. buildStrictGenericObjectMapper() is intentionally not customized.

Json / Yaml / Json31 / Yaml31 become replaceable

mapper() is unchanged in signature but is now lazily built and rebuilt when the factory generation moves, so a customizer registered at any time takes effect. New per-mapper API for the "only this one" case:

Json.addModule(module);                                   // this mapper only
Json.configure(b -> b.changeDefaultPropertyInclusion(...)); // rebuild from current config
Json.mapper(myMapper);                                    // install as-is (e.g. a framework-managed mapper); not customized further
Json.reset();                                             // back to factory defaults

Json31.converterMapper() gets the same treatment (+ converterMapper(ObjectMapper)). The four classes share a package-private MapperHolder.

ModelConverters keeps its default ModelResolver in sync

This is the part that actually restores Jackson 2 behaviour for model introspection. ModelConverters remembers how it built its default resolver and, when ObjectMapperFactory.generation() has moved, replaces only that resolver in place on the next read/readAll/resolveAsResolvedSchema/getConverters call. User-added converters, skipped packages/classes and the singleton itself are untouched. The stale _intr() javadoc in AbstractModelConverter is updated to describe this.

ObjectMapperProcessor is unchanged and composes with this: IntegrationObjectMapperFactory.createJson() now includes registered customizers.

Migration (Jackson 2 → 3.0.0)

Jackson 2 3.0.0
Json.mapper().registerModule(m) + same on Yaml/Json31/Yaml31 ObjectMapperFactory.addModule(m)
Json.mapper().registerModule(m) (JSON only, on purpose) Json.addModule(m)
Json.mapper().setSerializationInclusion(...) / .configure(...) / .addMixIn(...) Json.configure(b -> ...)
new ModelResolver(myMapper) unchanged

Note for the migration guide: Jackson 3 databind bundles JSR-310, JDK8 Optional and parameter-names support, so JavaTimeModule — historically the #1 reason to call registerModule — is no longer needed.

Tests

  • MapperCustomizerTest (11): module reaches all five targets; customizer ordering (user overrides NON_NULL); registering after first use rebuilds and re-caches; remove/clear; strict generic mapper untouched; per-mapper addModule/configure keep swagger mixins; explicit mapper(ObjectMapper) survives factory changes until reset(); Json31.reset() also resets the converter mapper.
  • ModelConvertersCustomizerTest (4): a SNAKE_CASE naming strategy registered after ModelConverters.getInstance() changes resolved property names (3.0 and 3.1 instances); user-added converter keeps its position across the refresh; removing the customizer restores defaults.

All new public classes/methods carry @since 3.0.0. Everything added is new static/instance API — no signature changes on the 3.0.0 branch.

Not in this PR (possible follow-ups): ServiceLoader discovery of MapperCustomizer, and a jacksonModules option for the maven/gradle plugins.

🤖 Generated with Claude Code

pjfanning and others added 3 commits September 19, 2026 14:45
Jackson 3 ObjectMappers are immutable, so the Jackson 2 idiom
Json.mapper().registerModule(...) no longer works, and the default
ModelResolver copies its mapper so a replaced Json.mapper() would not
reach model introspection either.

- ObjectMapperFactory: MapperCustomizer registry (addCustomizer,
  addModule(s), remove/clear, generation counter) applied as the last
  step of every mapper it builds, keyed by MapperTarget.
- Json/Yaml/Json31/Yaml31: mapper() is now lazily built and rebuilt when
  customizers change; new mapper(ObjectMapper), configure(...),
  addModule(...) and reset() per class (shared MapperHolder).
- ModelConverters: rebuilds only its default ModelResolver in place when
  the factory generation moves, keeping user-added converters.
- Fix stale _intr() javadoc in AbstractModelConverter.
- Tests: MapperCustomizerTest, ModelConvertersCustomizerTest.

Jackson 3 follow-up to swagger-api#4052 / swagger-api#4053; related swagger-api#3837, swagger-api#4991, swagger-api#5314.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Copilot review overview

🟡 Changes recommended

One or more issues must be addressed before approval.

Get a fresh assessment by requesting another Copilot review.

Review effort: Lite
Findings: 3 Medium severity

Open (3)
What changed in this PR

Adds Jackson 3 mapper customization APIs, replaceable shared mappers, and automatic ModelConverters refresh behavior.

Changes:

  • Adds global and per-mapper customizer/module APIs.
  • Rebuilds mappers when customizer generation changes.
  • Adds tests for mapper customization and model resolution refresh.
File Description
modules/​swagger-core/​src/​test/​java/​io/​swagger/​v3/​core/​util/​MapperCustomizerTest.java Updated as part of this pull request.
modules/​swagger-core/​src/​test/​java/​io/​swagger/​v3/​core/​resolving/​ModelConvertersCustomizerTest.java Updated as part of this pull request.
modules/​swagger-core/​src/​main/​java/​io/​swagger/​v3/​core/​util/​Yaml31.java Updated as part of this pull request.
modules/​swagger-core/​src/​main/​java/​io/​swagger/​v3/​core/​util/​Yaml.java Updated as part of this pull request.
modules/​swagger-core/​src/​main/​java/​io/​swagger/​v3/​core/​util/​ObjectMapperFactory.java Updated as part of this pull request.
modules/​swagger-core/​src/​main/​java/​io/​swagger/​v3/​core/​util/​MapperTarget.java Updated as part of this pull request.
modules/​swagger-core/​src/​main/​java/​io/​swagger/​v3/​core/​util/​MapperHolder.java Updated as part of this pull request.
modules/​swagger-core/​src/​main/​java/​io/​swagger/​v3/​core/​util/​MapperCustomizer.java Updated as part of this pull request.
modules/​swagger-core/​src/​main/​java/​io/​swagger/​v3/​core/​util/​Json31.java Updated as part of this pull request.
modules/​swagger-core/​src/​main/​java/​io/​swagger/​v3/​core/​util/​Json.java Updated as part of this pull request.
modules/​swagger-core/​src/​main/​java/​io/​swagger/​v3/​core/​jackson/​AbstractModelConverter.java Updated as part of this pull request.
modules/​swagger-core/​src/​main/​java/​io/​swagger/​v3/​core/​converter/​ModelConverters.java Updated as part of this pull request.

💡 Add a code-review agent skill or configure MCP servers for context-aware, tailored reviews. Learn more in the docs.

daniel-kmiecik and others added 2 commits September 25, 2026 14:42
Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com>
Add breaking change section to MIGRATION.md documenting that components
caching mappers at startup won't see customizers registered later.
Includes problem scenario, solution pattern, affected APIs, and
affected swagger-rest components (DefaultParameterExtension,
OpenAPI31SpecFilter). Also adds migration checklist item.

Related to PR swagger-api#5324 mapper customization API.
@daniel-kmiecik
daniel-kmiecik merged commit 1ed537e into swagger-api:3.0.0 Sep 25, 2026
6 checks passed
@pjfanning
pjfanning deleted the jackson3-mapper-customizers branch September 25, 2026 13:25
@pjfanning

Copy link
Copy Markdown
Contributor Author

@daniel-kmiecik thanks for merging this. I'm away for a few days so I am not in a position to act on the Copilot review items above. Without looking at them in detail, they do appear to raise some valid points.

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.

3 participants