Found while implementing #7113 (PR #7545); out of that card's scope — its ruling fences the change to chart-level data, xAxisKey, and the two series binding dialects. Filed unassigned for triage. Not fixed in that PR.
The finding
ChartDataSeriesSchema is a non-strict z.object — not .passthrough() like BaseSchema. A non-strict z.object strips an undeclared key in silence. Six keys that normalizeSeries demonstrably reads are undeclared, so a parsed series loses them and safeParse reports success.
Measured against the built mirror on origin/main 98d4108a2:
input : { name, label, stack, yAxis, opacity, dashArray, variant }
parse : success = true
output: { name }
Every one of the six is read at packages/plugin-charts/src/normalizeChartSchema.ts in normalizeSeries (lines 242-256):
| key |
read |
label |
const lbl = label(raw.label) |
variant |
str(raw.variant), narrowed to comparison / current / primary |
opacity |
num(raw.opacity) |
dashArray |
str(raw.dashArray) |
stack |
str(raw.stack) |
yAxis |
str(raw.yAxis), narrowed to left / right |
Why this is worse than the chart-level case #7113 fixed
They are different failure shapes, and this is the sharper one:
This is the same silent no-op the #6896 retirement tombstone comment names in this very file, applied to keys that are live rather than retired.
Also undeclared, though merely passed through rather than stripped, on ChartSchema itself: xAxis (the axis config object dialect — field, format, title, showGridLines, min, max, position, logarithmic) and yAxis, both read by normalizeAxis / normalizeChartSchema at lines 204-220 and 287-298. #7113 deliberately folded only the bare-string xAxis and left the object dialect untouched, because folding it would discard its presentation keys — declaring it is the follow-up this finding covers.
Suggested shape
Declare the six series keys on ChartDataSeriesSchema and its TS twin ChartDataSeries with the narrowings the reads already enforce (variant and yAxis are closed unions in the normalizer, so the declaration should be the union, not string), and declare the axis-config dialect on ChartSchema. Both faces move together or zod-mirror-parity.test.ts records the drift.
⚠️ Whether the axis-config object should be declared at all — versus retired under ADR-0049 if no authored document uses it — is a triage question, not a foregone conclusion. variant in particular is worth a liveness read before declaring: it is narrowed to three values whose consumers should be confirmed.
Related: #7113 (the chart-level half, PR #7545), #6896 (the retirement whose tombstone prose names this class), #7112 (the teaching sites).
Generated by Claude Code
Found while implementing #7113 (PR #7545); out of that card's scope — its ruling fences the change to chart-level
data,xAxisKey, and the two series binding dialects. Filed unassigned for triage. Not fixed in that PR.The finding
ChartDataSeriesSchemais a non-strictz.object— not.passthrough()likeBaseSchema. A non-strictz.objectstrips an undeclared key in silence. Six keys thatnormalizeSeriesdemonstrably reads are undeclared, so a parsed series loses them andsafeParsereports success.Measured against the built mirror on
origin/main98d4108a2:Every one of the six is read at
packages/plugin-charts/src/normalizeChartSchema.tsinnormalizeSeries(lines 242-256):labelconst lbl = label(raw.label)variantstr(raw.variant), narrowed tocomparison/current/primaryopacitynum(raw.opacity)dashArraystr(raw.dashArray)stackstr(raw.stack)yAxisstr(raw.yAxis), narrowed toleft/rightWhy this is worse than the chart-level case #7113 fixed
They are different failure shapes, and this is the sharper one:
ChartSchema,BaseSchemais.passthrough()) — an undeclared key is kept. It reached the renderer and rendered; only its value went unchecked. That is what [Decision]ChartSchema's real data model — chart-leveldataandxAxisKey— is undeclared, and #6896's retirement now points authors at it by name #7113 repaired fordata/xAxisKey.ChartDataSeriesSchema, non-strictz.object) — an undeclared key is removed. Any consumer of the parse output loses it outright.This is the same silent no-op the #6896 retirement tombstone comment names in this very file, applied to keys that are live rather than retired.
Also undeclared, though merely passed through rather than stripped, on
ChartSchemaitself:xAxis(the axis config object dialect —field,format,title,showGridLines,min,max,position,logarithmic) andyAxis, both read bynormalizeAxis/normalizeChartSchemaat lines 204-220 and 287-298. #7113 deliberately folded only the bare-stringxAxisand left the object dialect untouched, because folding it would discard its presentation keys — declaring it is the follow-up this finding covers.Suggested shape
Declare the six series keys on
ChartDataSeriesSchemaand its TS twinChartDataSerieswith the narrowings the reads already enforce (variantandyAxisare closed unions in the normalizer, so the declaration should be the union, notstring), and declare the axis-config dialect onChartSchema. Both faces move together orzod-mirror-parity.test.tsrecords the drift.variantin particular is worth a liveness read before declaring: it is narrowed to three values whose consumers should be confirmed.Related: #7113 (the chart-level half, PR #7545), #6896 (the retirement whose tombstone prose names this class), #7112 (the teaching sites).
Generated by Claude Code