🔍 fix: correct the last three findings from the page review

Of 26 findings raised across five auditors, 23 were refuted on a second
pass. These three survived.

**A renamed property's old key is still writable.** The README, the landing
page and two doc comments all said that once a property carries
@JsonProperty its original name "is no longer accepted on input". It is no
longer *mapped* — but it is not rejected either. Unlike @JsonReadOnly,
whose JSON name goes into the blocked set, the old key falls through to the
unknown-key policy, and the default `allow` copies it onto the instance
untouched. Reproduced against dist:

  @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
    -> round-trips out as home_address    <- silently accepted

Blocking the old key would fix it, but would also swallow the
`unknownKeys: 'error'` report a strict caller gets today, which is arguably
the more useful signal. That is a judgement call the library has not made,
so this commit states the behaviour accurately everywhere it was stated
wrongly and pins it with five tests covering the default, `strip`, `error`
and the @JsonAlias fix — so it cannot drift either way while the question
is open.

**@IsNotEmpty and @IsEmpty are not complements.** `[]` and `{}` pass BOTH:
isNotEmpty checks only null/undefined/'' while isEmpty also treats empty
arrays and objects as empty. Listed one line apart as "must not be empty" /
"must be empty", they invited exactly the wrong inference.

**unknownKeys is deserialization-only**, in a group whose blurb says these
apply per call or via configure().

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01SAcqrz3FcadkYr3xG32CjK
This commit is contained in:
Claude
2026-08-05 10:33:45 +00:00
parent 8c9aea3062
commit 057f008ac0
7 changed files with 136 additions and 12 deletions
+19
View File
@@ -114,6 +114,25 @@ Also corrected in the same pass: the toolchain table is described as executed by
the oxc row — the only ✗ — cannot be, because oxc ships inside a native binary with no
standalone transform API. The claim now covers the three rows it actually covers.
### A sharp edge, now documented and pinned
The README, the landing page and two doc comments all said that once a property carries
`@JsonProperty`, its original name "is no longer accepted on input". It is no longer
*mapped* — but it is not rejected either. Unlike `@JsonReadOnly`, whose JSON name goes into
the blocked set, a renamed property's old key falls through to the unknown-key policy, and
the default `allow` copies it onto the instance untouched. The value lands on a declared
property having skipped everything declared for it: no `@JsonType` conversion, so
`@ValidateNested` then inspects a plain object with no model and reports nothing.
Blocking the old key would fix that, but it would also swallow the `unknownKeys: 'error'`
report a strict caller gets today, which is arguably the more useful signal. That decision
has not been made, so the behaviour is stated accurately everywhere it was previously stated
wrongly, and five tests in `mapping.test.ts` pin it — including the `strip` and `error`
policies and the `@JsonAlias` fix — so it cannot change by accident either way.
Two reference entries were also imprecise: `@IsNotEmpty()` and `@IsEmpty()` read as
complements but are not (`[]` and `{}` pass both), and `unknownKeys` is deserialization-only.
### Positioning
`zod-alternative` is out of the keywords, and the README leads with the comparison that