Skip to content

#7918's relocated currency precision default is round-trip unsafe: a bare fixed-JPY currencyConfig materializes precision 2, and re-parsing that output is rejected #11423

Description

@os-steve

Filing unassigned — recording, not claiming. Measured on main-equivalent spec (merge head of PR #11406, but the mechanism shipped with the #7918 relocation and is independent of that PR) while diagnosing the CI red on #9689's PR.

Measured

FieldSchema.parse({ type: 'currency', currencyConfig: { currencyMode: 'fixed', defaultCurrency: 'JPY' } })
→ output currencyConfig.precision === 2 (the relocated default, materialized by .overwrite)
FieldSchema.safeParse(JSON.parse(JSON.stringify(output)))
→ REJECTED at currencyConfig.precision — "currency JPY has 0 fraction digits; `precision: 2` contradicts it"

Parse is not idempotent: parse(parse(x)) throws for an input parse(x) accepts. The #7918 Option A shape (.optional() + superRefine on the authored value + .overwrite() re-materializing the same default byte-identically) makes the materialized default indistinguishable from an authored value to the next parse — so any seam that re-parses parse output reads the baked default as authored and fires the per-type rejection.

Why this matters — the re-parse chain is the mainline authoring path

ObjectSchema.create() returns ObjectSchemaBase.parse(withDefaults) — parse OUTPUT. Every *.object.ts module exports create() output, and objectstack build's defineStack validation parses those objects again. #9689's PR #11406 hit exactly this with its master_detail set_null rejection: the showcase build fails on 4 fields that are BARE in source (objects.9.f_master_detail, objects.17.team, objects.17.project, objects.20.project) because parse #1 materialized set_null and parse #2 read it as authored. The currency twin has not fired only because no example app authors a bare currencyMode: 'fixed' + zero-fraction-digit currency config on a build path — the landmine is live for the first customer app that does (fixed + JPY/KRW/etc., no precision).

The class

A materialized default that the schema itself would refuse as authored breaks parse idempotency. Wherever the Option A shape guards a per-type/per-sibling-conditional value, the .overwrite() must not re-materialize the default into the exact configuration whose authored spelling the superRefine rejects (or the rejection must live at the authoring seam only, raw parse tolerant — the #9138/#8772 "Direction 2" precedent). Which resolution applies is being escalated for a ruling on #9689 (needs_decision, with the measured break list); this card records the currency instance so the same ruling can be applied here rather than re-litigated.

Refs: #7918 (the relocation and ruling), #9689 (the sibling instance that exposed the chain, PR #11406), #9138 / #8772 (the tolerant-raw-parse precedent).


Generated by Claude Code

Metadata

Metadata

Assignees

Type

Projects

No projects

Milestone

No milestone

Relationships

None yet

Development

No branches or pull requests

Issue actions