Fix @JsonAnyGetter keys not prefixed when inner bean uses @JsonUnwrapped(prefix=...) - #6123
Fix @JsonAnyGetter keys not prefixed when inner bean uses @JsonUnwrapped(prefix=...)#6123pjfanning wants to merge 15 commits into
@JsonAnyGetter keys not prefixed when inner bean uses @JsonUnwrapped(prefix=...)#6123Conversation
@JsonAnyGetter keys not prefixed when inner bean uses @JsonUnwrapped(prefix=...)
|
There is still an issue with deserialization of maps when unwrapped with prefix is used. The deserialization of the map doesn't remove the prefix. I want to treat that as a separate issue and can work out a fix for that if and when this serialization fix is merged. |
|
@cowtowncoder I fixed the deserialization issue with AI assistance. The issue I described above (#6123 (comment)) |
| * can be reverse-transformed before being passed to | ||
| * {@code @JsonAnySetter}. | ||
| * | ||
| * @since 3.3 |
There was a problem hiding this comment.
I have put version 3.3 on all the since markers but can change them to 3.1 or 3.2 if this is deemed safe for backporting
|
Ok. AI review pointed out a seemingly serious issue: Correctness issue — confirmed regression (high severity)AnyGetterWriter._serializeMapEntriesWithTransformer() (the new fallback used whenever _nameTransformer != null) completely bypasses _mapSerializer for real java.util.Map values, replacing it with a hand-rolled loop: rawKey.toString() for keys and ctxt.writeValue(gen, entryValue) for values. This silently drops everything MapSerializer normally provides:
This means: for any bean that combines I'll see if that can be fixed; that cannot be merged as-is. |
|
@cowtowncoder I got AI to sort some of the issues but there might be more. |
|
@pjfanning Hoping to pick this up soon, maybe tomorrow. |
3381453 to
06834b4
Compare
|
I got Claude Opus to write extra tests and it has 7 that fail and they are annotated so they don't run. Test coverage for
|
| # | Failure | Cause |
|---|---|---|
| 1 | {"a-obj":{"a-x":1,"a-y":2}} — nested Map/Array keys prefixed |
NameTransformingGenerator intercepts every writeName, at any depth, not just the any-getter's own keys |
| 2 | {"a-pt":{"a-x":1,"a-y":2}} — nested POJO properties prefixed |
same |
| 3 | {"a-pet":{"a-@type":"dog",…}} — polymorphic type id prefixed |
same |
| 4 | {"3":"x"} — Integer/Long keys get no prefix |
those go through JsonGenerator.writePropertyId(long), which the delegate doesn't override (String/enum/byte[] keys use writeName and do work) |
| 5 | Builder-based inner: any-setter receives a-age |
BuilderBasedDeserializer.unwrappingDeserializer() doesn't retain the NameTransformer the way BeanDeserializer now does |
| 6 | Two prefixed unwrapped beans: each any-setter collects the other's props (a.ea = {p=1, b-q=2}) |
when reverse() returns null the raw name is passed through instead of being skipped — writes are correct, so the value doesn't round-trip |
| 7 | Nested @JsonUnwrapped prefixes: a-b-age silently dropped on read |
pre-existing, but write now emits a-b-age, so the new serialization side has no working read side |
Provenance: items 1–4 are new with this branch — on the base commit (30e603d3f) this whole scenario
threw IllegalStateException: Method must be overridden by class AnyGetterWriter. Items 5 and 6 are
unchanged-but-wrong. Item 7 predates the branch (verified in a worktree at 30e603d3f).
Items 1–3 were checked against a non-unwrapped baseline, which correctly produces {"obj":{"x":1}}.
Suggested fixes
- 1–3 (the serious one) — a generator wrapper can't tell depth apart. Either make
NameTransformingGeneratortransform only at the depth it was installed (track
writeStartObject/writeEndObjectnesting, transform only when depth == 0), or drop the wrapper
and transform keys explicitly in aMapSerializerentry loop. - 4 — override
writePropertyId(long)inNameTransformingGenerator:
delegate.writeName(_transformer.transform(String.valueOf(id))). - 5 — mirror the
BeanDeserializerchange inBuilderBasedDeserializer. - 6 — skip the property (rather than passing the raw name) when
reverse()returnsnull.
Safe, since the no-prefix case usesNameTransformer.NOP, whosereverse()never returnsnull.
Code Review ✅ Approved 2 resolved / 2 findingsFixes ✅ 2 resolved✅ Bug: NameTransformingGenerator prefixes nested object keys too
✅ Performance: reverse() computed twice per accepted any-setter property
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 |
#6122
#6118
When
@JsonAnyGetterappears on a bean referenced via@JsonUnwrapped(prefix="..."), the prefix was never applied to the any-getter map keys. Worse,AnyGetterWriterdidn't overriderename(NameTransformer), so the inherited path called_new(PropertyName)which throwsIllegalStateExceptionfor subclasses — crashing serializer construction.Example
Changes
AnyGetterWriterNameTransformer _nameTransformerfield (null in normal, non-unwrapped use)AnyGetterWriter(AnyGetterWriter src, NameTransformer transformer)with correct chaining:chainedTransformer(newTransformer, src._nameTransformer), matching the ordering inUnwrappingBeanPropertyWriterrename(NameTransformer)to return a copy storing the transformer rather than attempting to rename the property itself (meaningless for any-getter)getAndSerialize()andgetAndFilter()now dispatch to transformer-aware helpers when_nameTransformer != null, applying it to each map/ObjectNodekey individuallyUnwrappedWithAnySetterTesttestUnwrappedWithPrefixWithAnyGettercovering the bug scenario: outer direct field serializes without prefix; inner any-getter entries get the"a-"prefix applied per key