Conversation
Opting in meant extending SealedPolymorphismSupport, which requires
editing the base type. That is not possible for a hierarchy from a
library or from generated code, so a root can be registered instead:
new SealedPolymorphismModule()
.registerSealedInterfaceOrClass(Animal.class)
.registerSealedInterfaceOrClass(Shape.class)
registerSealedInterfaceOrClass takes one type and is callable as often
as needed. Registering adds to what the module already handles through
the marker rather than replacing it, so marked and registered
hierarchies work through the same mapper.
A registered hierarchy is handled identically to a marked one - same
names, same reading, same conflict rules - and is held to the same
requirement that it be sealed. Registering null, a type that is not
sealed, an enum, or one carrying @JsonTypeInfo is refused by the
register call rather than later when Jackson first meets the type.
@JsonTypeInfo is refused because it already tells Jackson how to write
and read the hierarchy, so registering as well would ask for two type
properties at once. Nothing is relaxed below the root either: a
non-sealed member is still reported.
Register the root; implementations follow from its permits clause.
Registering part way down a hierarchy is allowed and makes that type the
root, so names are derived relative to it.
Which types are handled now depends on how the module was configured, so
the opt-in check and root resolution moved from statics to per-module
state, and registering discards the cache of which hierarchy a type
belongs to, since a type that resolved to nothing may now resolve to a
root. A hierarchy's name table is derived from the class files alone and
is identical however the hierarchy opted in, so that stays globally
cached, and the deserializers take the resolved hierarchy rather than
looking it up.
getRegistrationId includes what was registered, since Jackson drops a
module whose registration id it has already seen and two modules
registering different types are not the same module.
Rebased onto main after the enum work landed separately in #2. Enum
handling here is identical to main's, including that a marked enum is
ignored rather than reported.
19 new tests; 95 total.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_012hxYY9KPDAMXADWzjBHNV4
pjfanning
force-pushed
the
register-sealed-types
branch
from
August 23, 2026 19:19
d83d3f8 to
a342274
Compare
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Rebased onto
mainafter the enum work landed separately in #2. This PR now contains only the registration feature —git diff mainon the enum-behaviour files (SealedHierarchy,Fixtures,InvalidHierarchyTest) is empty.Opting in means extending
SealedPolymorphismSupport, which requires editing the base type. That is not possible for a hierarchy from a library or from generated code, so a root can be registered with the module instead.registerSealedInterfaceOrClasstakes one type and can be called as often as needed. Registering adds to what the module already handles through the marker rather than replacing it, so marked and registered hierarchies work through the same mapper.A registered hierarchy is handled identically to a marked one — same
@typenames, same reading, same@JsonTypeInfoconflict rules.What registration rejects
Rejected by
registerSealedInterfaceOrClassitself, rather than later when Jackson first meets the type:nullsealedenum@JsonTypeInfoThe
@JsonTypeInfocase is worth a note. On a marked hierarchy that annotation means "leave this to Jackson" and the module stands down. But registering is an explicit act, so asking for both is a contradiction rather than something to resolve silently — it would ask for two type properties on the same object.Nothing is relaxed below the root either: a
non-sealedmember of a registered hierarchy is still reported.Other behaviour
permitsclause. Registering part way down a hierarchy is allowed and makes that type the root, so names are derived relative to it and its siblings are left alone.main's, including that a marked enum is ignored rather than reported.Implementation
Which types are handled depends on module configuration, so the opt-in check and root resolution moved from statics to per-module state. Roots can be added after the module exists, so the registry and the root cache are mutable; registering discards that cache, since a type that previously resolved to nothing may now resolve to a root.
A hierarchy's name table is derived from the class files alone and is identical however the hierarchy opted in, so that stays globally cached. The deserializers now take the resolved hierarchy rather than looking it up, so they no longer need to know how it opted in.
getRegistrationIdincludes what was registered — Jackson drops a module whose registration id it has already seen, and two modules registering different types are not the same module.Tests
19 new tests — 18 in
RegisteredTypeTestagainst a newUnmarkedFixturesholding ordinary sealed hierarchies that do not reference this module at all, plus one inEnumsUntouchedTestcovering an enum in a registered hierarchy. 95 total, passing on JDK 17, 21 and 25.🤖 Generated with Claude Code
https://claude.ai/code/session_012hxYY9KPDAMXADWzjBHNV4