Skip to content

Two structured value classes (AddressSchema, LocationValueSchema) are all-optional stripping z.objects — a wrong key is accepted and dropped, so a stored-value scan reports a clean count it cannot earn #13802

Description

@os-warren

Split out of #13388 by the triage seat. That card is a one-key seed typo (postal_code where the contract says postalCode); this card is the reason the typo could not be caught, which has a different fix site, a different grade and a contract consequence the seed fix does not.

Measured on origin/main

packages/spec/src/data/field-value.zod.ts — no .strict() anywhere in the file. The structured value classes take three different postures, and two of them are the same accident:

Site Constructor Unknown key
:251 LocationValueSchema z.object({ … }) accepted, stripped
:271 AddressSchema (:282 AddressValueSchema = AddressSchema) z.object({ … }) accepted, stripped
:297 FileValueSchema z.looseObject({ … }) accepted, preserved — deliberate

Every member of the first two is optional. Combined with zod's strip-by-default z.object, that means a value with a completely wrong key set still parses successfully, and the wrong keys vanish from the parse output. #13388 demonstrates it end to end with the platform's own predicate:

valueSchemaFor({ type: 'address' }, 'stored').safeParse(
  { street: '1 Main St', city: 'Seattle', state: 'WA', postal_code: '98101', country: 'US' })
=> success: true, data: { street: '1 Main St', city: 'Seattle', state: 'WA', country: 'US' }

Why this is worth a card rather than a note

A stored-value scan over these two classes cannot report a violation — not "does not currently", but structurally cannot, for any wrong key whatsoever. A zero from such a scan is therefore not evidence of cleanliness; it is the only answer the instrument can give. #13388 is a live instance of drift that a scan would have counted as clean.

This is the false-green shape, one layer down from a gate: an instrument that reports a number it has no way to earn is worse than one that reports "unmeasurable", because the number gets carried forward into ledgers and summaries as a fact.

The fork, named and not chosen — this is the lane PM's call

  1. Make the two classes .strict(). Restores measurability, and is an accept-set narrowing on a value contract ⇒ Clause-② applies (path limb packages/spec/src/**, content limb: changes accept/reject behaviour). ⚠️ Existing stored values carrying extra keys would begin to fail; whether any exist is a deployment question this repo cannot answer, so this option needs a migration story or a strictness-ledger entry, not just a one-word edit.
  2. Leave the schemas and fix the instrument — have the scan report these two classes as unmeasurable rather than folding them into a violation count of zero. No contract change, no migration, and it makes the ledger honest immediately. Cheapest correct move.
  3. Adopt z.looseObject deliberately, as FileValueSchema already does, and document that structured value classes do not police their key set. Makes the current behaviour intentional rather than incidental, and closes the "is this a bug?" question without pretending the scan measures something.

Option 2 is not exclusive with 1 or 3 and is the recommended first step — it is the only one that does not require knowing what is in customer databases.

⛔ Out of scope

Re-check

git grep -n "strict()\|passthrough()\|looseObject(\|z.object(" -- packages/spec/src/data/field-value.zod.ts

⚠️ A zero from a bare strict() grep is only a reading if a known-present neighbour hits in the same query — the census above uses z.object( as that control.

Dedup

Targeted search this round: no open card on value-schema strictness. Nearest neighbours are #8687 (unknown top-level stack key not rejected) and #6326 (primaryField rejected by ObjectSchema's unrecognized_keys) — both about declaration surfaces rather than value surfaces, and both closed.

Related

Metadata

Metadata

Assignees

Labels

Type

No type

Projects

No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions