🔒 fix!: refuse names that no longer reach their property
A rename did not actually take effect. @JsonProperty stopped the old name
from being *mapped*, but not from being *accepted*: unlike @JsonReadOnly,
whose JSON name goes into the blocked set, the old key fell through to the
unknown-key policy, and the default `allow` copied it onto the instance
untouched.
The value therefore landed on a declared property having skipped everything
declared for it:
@JsonProperty('home_address') @JsonType(() => Addr) @ValidateNested()
address!: Addr;
toInstanceSync(Order, { address: { city: 'Paris' } })
-> address is a plain object, instanceof Addr === false
-> validateSync() returns [] <- nothing complains
A payload aimed at the previous version of a class was accepted in part, in
silence. Three routes led to the same hole, and all three are now closed:
- the property key of a field renamed with @JsonProperty
- the raw key of a field a naming strategy renders differently
(`firstName` under snake_case)
- the property key of a field that is both renamed and @JsonReadOnly, which
was still settable under its own key
Refused, not swallowed. A stale name is a mismatch with whatever produced
the payload, not a deliberate refusal like @JsonReadOnly, so it is kept in
its own map rather than lumped into `blocked`: under unknownKeys: 'error'
it is still reported, and the report now names the property it was reaching
for and what that property is called now.
"ref" is not a JSON name for Order: property "ref" is mapped to
"order_ref". Send that name, or add @JsonAlias("ref") to keep
accepting this one.
@JsonAlias still keeps an old name working, and a key that some *other*
property legitimately answers to is still mapped to that property — both
asserted. The resolution happens once when the name map is built, which is
memoized per class and naming strategy, so deserialization is unchanged at
~45µs for 50 nested orders.
The behaviour this replaces was pinned by tests two commits ago, pending
this decision; those tests now assert the fix, and six more cover the
naming-strategy, read-only, alias and key-collision cases.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01SAcqrz3FcadkYr3xG32CjK
This commit is contained in:
+4
-6
@@ -814,12 +814,10 @@ npm install ../cereale/cereale-0.3.0.tgz</code></pre>
|
||||
<div class="note-item">
|
||||
<h3>Renaming is not backwards-compatible by itself</h3>
|
||||
<p>Once a property carries <code class="inline-code">@JsonProperty</code>, its original name
|
||||
no longer <em>maps</em> to it — but under the default
|
||||
<code class="inline-code">unknownKeys: 'allow'</code> it is not rejected either. It is
|
||||
copied onto the instance raw, skipping any
|
||||
<code class="inline-code">@JsonType</code> or <code class="inline-code">@JsonDeserialize</code>
|
||||
conversion declared for that field. Add <code class="inline-code">@JsonAlias</code> to keep
|
||||
older clients working, or <code class="inline-code">unknownKeys: 'strip'</code> to drop them.</p>
|
||||
no longer reaches it — it is refused rather than copied onto the instance behind the
|
||||
rename's back. Add <code class="inline-code">@JsonAlias</code> to keep older clients
|
||||
working. Under <code class="inline-code">unknownKeys: 'error'</code> the stale name is
|
||||
reported by name, along with what the property is called now.</p>
|
||||
</div>
|
||||
<div class="note-item">
|
||||
<h3><code class="inline-code">abstract</code> and <code class="inline-code">accessor</code> fields cannot be decorated</h3>
|
||||
|
||||
Reference in New Issue
Block a user