Skip to content

describe a bool or DateTime map key as the string serde writes, where the key's own type is no property key - #236

Merged
eburgos merged 1 commit into
masterfrom
fix/non-property-map-keys
Aug 12, 2026
Merged

describe a bool or DateTime map key as the string serde writes, where the key's own type is no property key#236
eburgos merged 1 commit into
masterfrom
fix/non-property-map-keys

Conversation

@eburgos

@eburgos eburgos commented Aug 12, 2026

Copy link
Copy Markdown
Owner

A map keyed by bool or DateTime<Tz> was described at the key's own type on
both nominal surfaces, and neither survives contact with a compiler or a
payload. Partial<Record<boolean, V>> and Partial<Record<Date, V>> are hard
TS2344 — neither type is a property key — and the emitted Zod fails the same
way at its own construction. At runtime the bool key was rejected outright
(z.boolean() handed the string "true"), and the DateTime key was worse:
z.coerce.date() accepted it and rewrote every key to a locale-dependent
Date.toString(), corrupting the map silently with no round-trip.

serde writes both keys as strings — true as "true", a DateTime as its
RFC 3339 text — and the fix is to describe that wire instead of the type:

bool      Partial<Record<"true" | "false", V>>
          z.partialRecord(z.enum(["true", "false"]), V)
DateTime  Partial<Record<string, V>>
          z.record(z.iso.datetime({ offset: true }), V)

The answer comes from one seam both surfaces read, and a name forwards its
target's answer, so a brand over bool, an alias over DateTime, and an alias
of a brand all render the same wire form their inner does. The registry entry
that had collapsed brand-over-bool with brand-over-number now carries the wire
form beside the kind it already recorded. The Zod side yields the whole record
call rather than the key alone, since the constructor moves with the key. JSON
Schema is untouched — its open object never claimed the key's type. The
self-referencing spellings compose: a bool key takes the mapped-type branch,
a DateTime key the index signature.

Verified on the verbatim emitted text: tsc strict exits 0 where it produced
eleven errors, and zod 4.4.3 parses the real serde payload with every key
preserved byte for byte. Both constructors exist at zod 4.0.0, the documented
floor. String-, number- and enum-keyed maps are pinned byte-identical.

just fmt, just check, just quick, just lint-all across all 68 toggles, and the
test powerset across all 68 in four partitions — all green.

… the key's own type is no property key

A map keyed by `bool` or `DateTime<Tz>` was described at the key's own type on
both nominal surfaces, and neither survives contact with a compiler or a
payload. `Partial<Record<boolean, V>>` and `Partial<Record<Date, V>>` are hard
TS2344 — neither type is a property key — and the emitted Zod fails the same
way at its own construction. At runtime the bool key was rejected outright
(`z.boolean()` handed the string `"true"`), and the DateTime key was worse:
`z.coerce.date()` accepted it and rewrote every key to a locale-dependent
`Date.toString()`, corrupting the map silently with no round-trip.

serde writes both keys as strings — `true` as `"true"`, a `DateTime` as its
RFC 3339 text — and the fix is to describe that wire instead of the type:

    bool      Partial<Record<"true" | "false", V>>
              z.partialRecord(z.enum(["true", "false"]), V)
    DateTime  Partial<Record<string, V>>
              z.record(z.iso.datetime({ offset: true }), V)

The answer comes from one seam both surfaces read, and a name forwards its
target's answer, so a brand over `bool`, an alias over `DateTime`, and an alias
of a brand all render the same wire form their inner does. The registry entry
that had collapsed brand-over-bool with brand-over-number now carries the wire
form beside the kind it already recorded. The Zod side yields the whole record
call rather than the key alone, since the constructor moves with the key. JSON
Schema is untouched — its open object never claimed the key's type. The
self-referencing spellings compose: a bool key takes the mapped-type branch,
a DateTime key the index signature.

Verified on the verbatim emitted text: tsc strict exits 0 where it produced
eleven errors, and zod 4.4.3 parses the real serde payload with every key
preserved byte for byte. Both constructors exist at zod 4.0.0, the documented
floor. String-, number- and enum-keyed maps are pinned byte-identical.

just fmt, just check, just quick, just lint-all across all 68 toggles, and the
test powerset across all 68 in four partitions — all green.
@eburgos
eburgos merged commit 170f424 into master Aug 12, 2026
@eburgos
eburgos deleted the fix/non-property-map-keys branch August 12, 2026 00:40
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant