Fix #6188: resolve standard Throwable property names against PropertyNamingStrategy - #6189
Conversation
…rtyNamingStrategy` `ThrowableDeserializer` matched the standard `Throwable` property names against hard-coded canonical names using `equalsIgnoreCase()`, which absorbs case-changing renames but cannot match snake-/kebab-cased ones. Resolve the five external names once, at construction, by passing each canonical name and its real accessor to the configured strategy -- the way `buildThrowableDeserializer()` already resolves "cause". Completes the TODO left by #3497. Fixes three symptoms under such a strategy: * "localized_message" reported as an unknown property (throws with FAIL_ON_UNKNOWN_PROPERTIES) * "stack_trace": null not skipped, so `setStackTrace(null)` throws NPE * view-filtered "stack_trace" dropped from input, since the standard-property exemption added by #6174 missed the renamed name Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01ViTmN932ZXuhpABmeGa6L9
… strategy Review of #6189 found that reading `config.getPropertyNamingStrategy()` directly misses class-level `@JsonNaming`, which `POJOPropertiesCollector._findNamingStrategy()` resolves first -- including its `PropertyNamingStrategy.class` "use default" pseudo-value, which overrides the mapper-level strategy. Two consequences, both verified: * `@JsonNaming(PropertyNamingStrategy.class)` on the class plus a SNAKE_CASE mapper resolved "stack_trace" while the properties were really canonical, so `_shouldSkipNullValue()` missed and `setStackTrace(null)` threw NPE. This passed before #6189 -- a regression introduced by it. * `@JsonNaming(SnakeCaseStrategy.class)` with no mapper-level strategy resolved canonical names while the properties were snake_cased, leaving the very symptoms #6188 set out to fix. Take the names from the property definitions introspection already produced, instead of re-deriving them: `beanDesc.findProperties()` has them fully resolved, so a mapper-level strategy, `@JsonNaming` and an explicit `@JsonProperty` rename are all accounted for. Accessors are still located by signature, so a rename cannot hide them. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01ViTmN932ZXuhpABmeGa6L9
|
Review follow-up ( What was wrong:
Fix: stop re-deriving the names. Two regression tests added for the One behavior change worth calling out explicitly, which the original description omitted: comparisons now use the resolved names, so under a renaming strategy the canonical spelling is no longer specially recognized. Concretely, with Full suite: 6250 tests, 0 failures. |
Code Review ✅ ApprovedResolves OptionsAuto-apply is off → Gitar will not commit updates to this branch. Comment with these commands to change the behavior for this request:
Was this helpful? React with 👍 / 👎 | Powered by Gitar — free for open source |
Fixes #6188.
ThrowableDeserializercompared the standardThrowableproperty names ("message", "localizedMessage", "suppressed", "cause", "stackTrace") against hard-coded canonical names withequalsIgnoreCase(). That absorbs case-changing renames, but cannot match snake- or kebab-cased ones —"stack_trace".equalsIgnoreCase("stackTrace")isfalse— so under such aPropertyNamingStrategythe two multi-word names went unrecognized.This completes the TODO left in
construct()when #3497 was closed after fixing only thecausehalf:Approach
Resolve the five external names once, at construction, instead of comparing canonical names at read time.
construct()already receives theDeserializationContextand has a single call site, soBeanDeserializerFactory.buildThrowableDeserializer()now also passes theBeanDescription.Supplier. Each canonical name is run through the naming strategy together with its real accessor — found reflectively viabeanDesc.findMethod("setStackTrace", ...),findMethod("initCause", ...)and so on — exactly the way that method already resolvescause:Handing the strategy the actual member (rather than matching accessor names at read time) keeps custom strategies that inspect the member working. When no strategy is configured, the canonical names are used as-is and nothing changes.
Fixes
Three symptoms, each covered by a test verified to fail without the change:
localized_messagetreated as an unknown propertyUnrecognizedPropertyExceptionwithFAIL_ON_UNKNOWN_PROPERTIES"stack_trace": nullreachessetStackTrace(null)NullPointerExceptionstack_tracedropped from inputThe third is a regression from #6174 (3.1.7), whose standard-property exemption matched by canonical name; the first two are long-standing.
message,causeandsuppressedwere unaffected in practice — single words, so snake/kebab leave them unchanged andequalsIgnoreCase()coveredUPPER_CAMEL_CASE— but they are resolved too, since a custom strategy could rename them.Notes
construct()is kept as a deprecated delegate (passingnull, i.e. canonical names) rather than having its signature changed, since it ispublic staticand reachable by a customDeserializerFactory. Happy to drop it instead.26-May-2022 ... let's cheatcomment, which described the behavior this change replaces.Full suite: 6248 tests, 0 failures.
🤖 Generated with Claude Code
https://claude.ai/code/session_01ViTmN932ZXuhpABmeGa6L9