Skip to content

the defaultValue literal gate prescribes the missing-member type error, not the key rename the schema carries #16077

Description

@claude

Found while fixing #15490 (which retired the same positional read on the stored-value path). Not touched there: it is a different function, on a different authoring surface, so it is filed rather than ridden along.

The defect

checkLiteralDefaultValue (packages/spec/src/data/default-value-shape.ts:120) reads a value-contract rejection positionally:

export function checkLiteralDefaultValue(def: ValueShapeFieldDef, dv: unknown): LiteralDefaultValueVerdict {
  const result = valueSchemaFor(def, 'stored').safeParse(dv);
  if (result.success) return { ok: true };
  return { ok: false, detail: result.error.issues[0]?.message ?? 'invalid value' };
}

LiteralDefaultValueVerdict.detail documents itself as "First issue message from the value contract — the 'why' a refusal carries verbatim." zod sorts per-member issues ahead of the object-level unrecognized_keys one, so when an author writes a defaultValue with a RENAMED key, the message that names the rename is discarded and the author is handed a missing-member type error instead.

Measured on origin/main at c463d03e0, against the built @objectstack/spec

checkLiteralDefaultValue({type:'location'}, {latitude:1, longitude:2})
   detail -> "Invalid input: expected number, received undefined"
   names the rename? false

checkLiteralDefaultValue({type:'address'}, {street:5, postal_code:'98101'})
   detail -> "Invalid input: expected string, received number"
   names the rename? false

checkLiteralDefaultValue({type:'address'}, {postal_code:'98101'})       <- no type error to sort ahead
   detail -> "Unrecognized key(s) on this address value: `postal_code`. Did you mean `postal_code` -> `postalCode`? ..."
   names the rename? true

Same asymmetry #15490 documented: the diagnosis quality depends on whether some unrelated member happened to also be wrong, which nobody chose and nobody can see.

Why it is a separate card

This is the authoring-time gate for defaultValue — the shared core of the action-param gate and the field gate — not the stored-value scan. It is a different surface with a different audience (a metadata author writing a declaration, not an operator running a migration), and LiteralDefaultValueVerdict.detail is its own documented contract. #15490's fix is scoped to record-validator.ts and deliberately did not widen into packages/spec.

Suggested direction (not a decision)

The same shape #15490 landed: prefer the unrecognized_keys issue when the rejection carries one. The sweep done for #15490 measured all 16 classes reachable through valueSchemaFor and found only location and address able to emit unrecognized_keys at all, so the preference is a no-op for every other class — that sweep should carry over here, since this function calls the same valueSchemaFor(def, 'stored'). Worth confirming that no caller of checkLiteralDefaultValue pins the current text.

Provenance: #15490 (the stored-value half, and the sweep).


Generated by Claude Code

Activity

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions