[{"data":1,"prerenderedAt":772},["ShallowReactive",2],{"content-\u002Fdocs\u002Fschemas\u002Fdiscriminated-unions":3},{"id":4,"title":5,"body":6,"description":752,"extension":753,"meta":754,"metaRows":755,"navigation":91,"path":767,"seo":768,"source":769,"stem":770,"__hash__":771},"docs\u002Fdocs\u002Fschemas\u002Fdiscriminated-unions.md","Discriminated unions",{"type":7,"value":8,"toc":743},"minimark",[9,13,25,28,35,39,44,237,248,252,361,364,377,388,392,399,402,421,432,436,447,604,619,623,628,678,694,698,709,713,739],[10,11,5],"h1",{"id":12},"discriminated-unions",[14,15,16],"blockquote",{},[17,18,19,20,24],"p",{},"Schemas that branch on a discriminator key get first-class runtime handling: switching the discriminator reshapes storage to the new variant's slim default, and ",[21,22,23],"code",{},"form.errors.\u003Cpath>"," is automatically variant-filtered.",[26,27],"docs-meta-table",{},[17,29,30,31,34],{},"Pick a notify channel (Email, SMS, or Push) and watch the field below the radios swap to the variant's typed shape. The schema's discriminated union drives both the conditional render and the underlying storage reshape; whatever the inactive variant held gets purged from ",[21,32,33],{},"form.values",", and the new variant's slim default seeds.",[36,37],"docs-demo",{"label":38,"slug":12},"Discriminated Union Demo",[40,41,43],"h2",{"id":42},"the-schema","The schema",[45,46,51],"pre",{"className":47,"code":48,"language":49,"meta":50,"style":50},"language-ts shiki shiki-themes github-light github-dark","import { useForm } from 'attaform\u002Fzod'\nimport { z } from 'zod'\n\nconst schema = z.object({\n  notify: z.discriminatedUnion('channel', [\n    z.object({ channel: z.literal('email'), address: z.email() }),\n    z.object({ channel: z.literal('sms'), phone: z.string() }),\n    z.object({ channel: z.literal('push'), deviceId: z.string() }),\n  ]),\n})\nconst form = useForm({ schema })\n","ts","",[21,52,53,73,86,93,116,134,162,186,209,215,221],{"__ignoreMap":50},[54,55,58,62,66,69],"span",{"class":56,"line":57},"line",1,[54,59,61],{"class":60},"szBVR","import",[54,63,65],{"class":64},"sVt8B"," { useForm } ",[54,67,68],{"class":60},"from",[54,70,72],{"class":71},"sZZnC"," 'attaform\u002Fzod'\n",[54,74,76,78,81,83],{"class":56,"line":75},2,[54,77,61],{"class":60},[54,79,80],{"class":64}," { z } ",[54,82,68],{"class":60},[54,84,85],{"class":71}," 'zod'\n",[54,87,89],{"class":56,"line":88},3,[54,90,92],{"emptyLinePlaceholder":91},true,"\n",[54,94,96,99,103,106,109,113],{"class":56,"line":95},4,[54,97,98],{"class":60},"const",[54,100,102],{"class":101},"sj4cs"," schema",[54,104,105],{"class":60}," =",[54,107,108],{"class":64}," z.",[54,110,112],{"class":111},"sScJk","object",[54,114,115],{"class":64},"({\n",[54,117,119,122,125,128,131],{"class":56,"line":118},5,[54,120,121],{"class":64},"  notify: z.",[54,123,124],{"class":111},"discriminatedUnion",[54,126,127],{"class":64},"(",[54,129,130],{"class":71},"'channel'",[54,132,133],{"class":64},", [\n",[54,135,137,140,142,145,148,150,153,156,159],{"class":56,"line":136},6,[54,138,139],{"class":64},"    z.",[54,141,112],{"class":111},[54,143,144],{"class":64},"({ channel: z.",[54,146,147],{"class":111},"literal",[54,149,127],{"class":64},[54,151,152],{"class":71},"'email'",[54,154,155],{"class":64},"), address: z.",[54,157,158],{"class":111},"email",[54,160,161],{"class":64},"() }),\n",[54,163,165,167,169,171,173,175,178,181,184],{"class":56,"line":164},7,[54,166,139],{"class":64},[54,168,112],{"class":111},[54,170,144],{"class":64},[54,172,147],{"class":111},[54,174,127],{"class":64},[54,176,177],{"class":71},"'sms'",[54,179,180],{"class":64},"), phone: z.",[54,182,183],{"class":111},"string",[54,185,161],{"class":64},[54,187,189,191,193,195,197,199,202,205,207],{"class":56,"line":188},8,[54,190,139],{"class":64},[54,192,112],{"class":111},[54,194,144],{"class":64},[54,196,147],{"class":111},[54,198,127],{"class":64},[54,200,201],{"class":71},"'push'",[54,203,204],{"class":64},"), deviceId: z.",[54,206,183],{"class":111},[54,208,161],{"class":64},[54,210,212],{"class":56,"line":211},9,[54,213,214],{"class":64},"  ]),\n",[54,216,218],{"class":56,"line":217},10,[54,219,220],{"class":64},"})\n",[54,222,224,226,229,231,234],{"class":56,"line":223},11,[54,225,98],{"class":60},[54,227,228],{"class":101}," form",[54,230,105],{"class":60},[54,232,233],{"class":111}," useForm",[54,235,236],{"class":64},"({ schema })\n",[17,238,239,240,243,244,247],{},"Three variants, one discriminator (",[21,241,242],{},"channel","). Each variant is a regular ",[21,245,246],{},"z.object","; refinements, defaults, transforms, nested objects all work the way they do anywhere else.",[40,249,251],{"id":250},"what-happens-on-a-switch","What happens on a switch",[45,253,255],{"className":47,"code":254,"language":49,"meta":50,"style":50},"form.setValue('notify.channel', 'email')\nform.setValue('notify.address', 'a@b.com')\n\u002F\u002F storage: { notify: { channel: 'email', address: 'a@b.com' } }\n\nform.setValue('notify.channel', 'sms')\n\u002F\u002F storage: { notify: { channel: 'sms', phone: '' } }\n\u002F\u002F   (email's `address` is purged; sms's `phone` is seeded)\n\nform.setValue('notify.channel', 'email')\n\u002F\u002F storage: { notify: { channel: 'email', address: 'a@b.com' } }\n\u002F\u002F   (`address` restored from variant memory)\n",[21,256,257,278,296,302,306,322,327,332,336,352,356],{"__ignoreMap":50},[54,258,259,262,265,267,270,273,275],{"class":56,"line":57},[54,260,261],{"class":64},"form.",[54,263,264],{"class":111},"setValue",[54,266,127],{"class":64},[54,268,269],{"class":71},"'notify.channel'",[54,271,272],{"class":64},", ",[54,274,152],{"class":71},[54,276,277],{"class":64},")\n",[54,279,280,282,284,286,289,291,294],{"class":56,"line":75},[54,281,261],{"class":64},[54,283,264],{"class":111},[54,285,127],{"class":64},[54,287,288],{"class":71},"'notify.address'",[54,290,272],{"class":64},[54,292,293],{"class":71},"'a@b.com'",[54,295,277],{"class":64},[54,297,298],{"class":56,"line":88},[54,299,301],{"class":300},"sJ8bj","\u002F\u002F storage: { notify: { channel: 'email', address: 'a@b.com' } }\n",[54,303,304],{"class":56,"line":95},[54,305,92],{"emptyLinePlaceholder":91},[54,307,308,310,312,314,316,318,320],{"class":56,"line":118},[54,309,261],{"class":64},[54,311,264],{"class":111},[54,313,127],{"class":64},[54,315,269],{"class":71},[54,317,272],{"class":64},[54,319,177],{"class":71},[54,321,277],{"class":64},[54,323,324],{"class":56,"line":136},[54,325,326],{"class":300},"\u002F\u002F storage: { notify: { channel: 'sms', phone: '' } }\n",[54,328,329],{"class":56,"line":164},[54,330,331],{"class":300},"\u002F\u002F   (email's `address` is purged; sms's `phone` is seeded)\n",[54,333,334],{"class":56,"line":188},[54,335,92],{"emptyLinePlaceholder":91},[54,337,338,340,342,344,346,348,350],{"class":56,"line":211},[54,339,261],{"class":64},[54,341,264],{"class":111},[54,343,127],{"class":64},[54,345,269],{"class":71},[54,347,272],{"class":64},[54,349,152],{"class":71},[54,351,277],{"class":64},[54,353,354],{"class":56,"line":217},[54,355,301],{"class":300},[54,357,358],{"class":56,"line":223},[54,359,360],{"class":300},"\u002F\u002F   (`address` restored from variant memory)\n",[17,362,363],{},"Writing the discriminator triggers a structural reshape:",[365,366,367,371,374],"ol",{},[368,369,370],"li",{},"The outgoing variant's keys are purged from storage.",[368,372,373],{},"The new variant's keys are seeded from the schema's slim defaults.",[368,375,376],{},"The structural-completeness invariant runs before any subsequent reads: missing variant keys get filled even if the write was the discriminator only.",[17,378,379,380,383,384,387],{},"The reshape fires for every variant write: ",[21,381,382],{},"setValue('notify', { channel: 'sms', phone: '' })"," reshapes the same way as ",[21,385,386],{},"setValue('notify.channel', 'sms')",".",[40,389,391],{"id":390},"variant-memory","Variant memory",[17,393,394,395,398],{},"Switching back to a previously-visited variant restores its prior typed subtree by default; ",[21,396,397],{},"rememberVariants: true"," is Attaform's default. The \"memory\" lives in-memory only (not persisted across reloads); each discriminated union at every nesting depth memorizes independently.",[17,400,401],{},"Opt out per-form:",[45,403,405],{"className":47,"code":404,"language":49,"meta":50,"style":50},"useForm({ schema, rememberVariants: false })\n",[21,406,407],{"__ignoreMap":50},[54,408,409,412,415,418],{"class":56,"line":57},[54,410,411],{"class":111},"useForm",[54,413,414],{"class":64},"({ schema, rememberVariants: ",[54,416,417],{"class":101},"false",[54,419,420],{"class":64}," })\n",[17,422,423,424,426,427,431],{},"With ",[21,425,417],{},", every switch drops the outgoing variant's typed state. See ",[428,429,391],"a",{"href":430},"\u002Fdocs\u002Fwriting-and-mutating\u002Fvariant-memory"," for the full discussion.",[40,433,435],{"id":434},"template-rendering","Template rendering",[17,437,438,439,442,443,446],{},"Branch on the discriminator value with ",[21,440,441],{},"v-if"," \u002F ",[21,444,445],{},"v-else-if",":",[45,448,452],{"className":449,"code":450,"language":451,"meta":50,"style":50},"language-vue shiki shiki-themes github-light github-dark","\u003Ctemplate>\n  \u003Clabel v-if=\"form.values.notify.channel === 'email'\">\n    \u003Cinput v-register=\"form.register('notify.address')\" \u002F>\n    \u003Cem v-if=\"form.fields.notify.address.showErrors\">\n      {{ form.fields.notify.address.firstError?.message }}\n    \u003C\u002Fem>\n  \u003C\u002Flabel>\n\n  \u003Clabel v-else-if=\"form.values.notify.channel === 'sms'\">\n    \u003Cinput v-register=\"form.register('notify.phone')\" type=\"tel\" \u002F>\n  \u003C\u002Flabel>\n\u003C\u002Ftemplate>\n","vue",[21,453,454,466,485,504,520,525,534,543,547,563,586,594],{"__ignoreMap":50},[54,455,456,459,463],{"class":56,"line":57},[54,457,458],{"class":64},"\u003C",[54,460,462],{"class":461},"s9eBZ","template",[54,464,465],{"class":64},">\n",[54,467,468,471,474,477,480,483],{"class":56,"line":75},[54,469,470],{"class":64},"  \u003C",[54,472,473],{"class":461},"label",[54,475,476],{"class":111}," v-if",[54,478,479],{"class":64},"=",[54,481,482],{"class":71},"\"form.values.notify.channel === 'email'\"",[54,484,465],{"class":64},[54,486,487,490,493,496,498,501],{"class":56,"line":88},[54,488,489],{"class":64},"    \u003C",[54,491,492],{"class":461},"input",[54,494,495],{"class":111}," v-register",[54,497,479],{"class":64},[54,499,500],{"class":71},"\"form.register('notify.address')\"",[54,502,503],{"class":64}," \u002F>\n",[54,505,506,508,511,513,515,518],{"class":56,"line":95},[54,507,489],{"class":64},[54,509,510],{"class":461},"em",[54,512,476],{"class":111},[54,514,479],{"class":64},[54,516,517],{"class":71},"\"form.fields.notify.address.showErrors\"",[54,519,465],{"class":64},[54,521,522],{"class":56,"line":118},[54,523,524],{"class":64},"      {{ form.fields.notify.address.firstError?.message }}\n",[54,526,527,530,532],{"class":56,"line":136},[54,528,529],{"class":64},"    \u003C\u002F",[54,531,510],{"class":461},[54,533,465],{"class":64},[54,535,536,539,541],{"class":56,"line":164},[54,537,538],{"class":64},"  \u003C\u002F",[54,540,473],{"class":461},[54,542,465],{"class":64},[54,544,545],{"class":56,"line":188},[54,546,92],{"emptyLinePlaceholder":91},[54,548,549,551,553,556,558,561],{"class":56,"line":211},[54,550,470],{"class":64},[54,552,473],{"class":461},[54,554,555],{"class":111}," v-else-if",[54,557,479],{"class":64},[54,559,560],{"class":71},"\"form.values.notify.channel === 'sms'\"",[54,562,465],{"class":64},[54,564,565,567,569,571,573,576,579,581,584],{"class":56,"line":217},[54,566,489],{"class":64},[54,568,492],{"class":461},[54,570,495],{"class":111},[54,572,479],{"class":64},[54,574,575],{"class":71},"\"form.register('notify.phone')\"",[54,577,578],{"class":111}," type",[54,580,479],{"class":64},[54,582,583],{"class":71},"\"tel\"",[54,585,503],{"class":64},[54,587,588,590,592],{"class":56,"line":223},[54,589,538],{"class":64},[54,591,473],{"class":461},[54,593,465],{"class":64},[54,595,597,600,602],{"class":56,"line":596},12,[54,598,599],{"class":64},"\u003C\u002F",[54,601,462],{"class":461},[54,603,465],{"class":64},[17,605,606,607,610,611,614,615,618],{},"Vue's template narrowing follows the discriminator: autocomplete on ",[21,608,609],{},"form.values.notify.address"," only suggests when the branch matches the literal type. The ",[21,612,613],{},"form.fields.\u003Cvariant-path>"," access works regardless of the active variant, but ",[21,616,617],{},"showErrors"," only fires when the path is reachable (so an inactive variant's stale field state stays silent).",[40,620,622],{"id":621},"errors-are-variant-filtered","Errors are variant-filtered",[17,624,625,627],{},[21,626,23],{}," (the per-leaf view) filters by active variant: only errors on the active variant's path show up. Inline field rendering doesn't need any extra guards:",[45,629,631],{"className":449,"code":630,"language":451,"meta":50,"style":50},"\u003Csmall v-if=\"form.errors.notify.address?.[0]\">\n  \u003C!-- only renders when notify.channel === 'email' -->\n  {{ form.errors.notify.address[0].message }}\n\u003C\u002Fsmall>\n",[21,632,633,660,665,670],{"__ignoreMap":50},[54,634,635,637,640,642,644,647,650,653,656,658],{"class":56,"line":57},[54,636,458],{"class":64},[54,638,639],{"class":461},"small",[54,641,476],{"class":60},[54,643,479],{"class":64},[54,645,646],{"class":71},"\"",[54,648,649],{"class":64},"form.errors.notify.address?.[",[54,651,652],{"class":101},"0",[54,654,655],{"class":64},"]",[54,657,646],{"class":71},[54,659,465],{"class":64},[54,661,662],{"class":56,"line":75},[54,663,664],{"class":64},"  \u003C!-- only renders when notify.channel === 'email' -->\n",[54,666,667],{"class":56,"line":88},[54,668,669],{"class":64},"  {{ form.errors.notify.address[0].message }}\n",[54,671,672,674,676],{"class":56,"line":95},[54,673,599],{"class":64},[54,675,639],{"class":461},[54,677,465],{"class":64},[17,679,680,681,684,685,689,690,693],{},"The form-level ",[21,682,683],{},"form.meta.errors"," aggregate is ",[686,687,688],"strong",{},"NOT"," variant-filtered; errors for the inactive variant's fields stay in the array. A \"show all\" wizard summary iterating ",[21,691,692],{},"meta.errors"," will surface stale errors for the variant the user left. For wizard-wide summaries, prefer per-path reads or filter the aggregate manually.",[40,695,697],{"id":696},"invalid-discriminator-values","Invalid discriminator values",[17,699,700,701,704,705,708],{},"If the user types a discriminator value that doesn't match any variant (",[21,702,703],{},"channel: 'fax'"," against ",[21,706,707],{},"'email' | 'sms' | 'push'","), the reshape is skipped; the variant fields stay as they were, and the schema surfaces a refinement error on the discriminator itself. Template variant-conditional rendering can branch on the schema's kind rather than relying on the runtime to \"guess\" a variant.",[40,710,712],{"id":711},"where-to-next","Where to next",[714,715,716,721,730],"ul",{},[368,717,718,720],{},[428,719,391],{"href":430},": when to keep the prior typed subtree, when to drop it.",[368,722,723,729],{},[428,724,726,728],{"href":725},"\u002Fdocs\u002Fwriting-and-mutating\u002Fset-value",[21,727,264],{}," patterns",": programmatic writes that drive the same reshape.",[368,731,732,738],{},[428,733,735],{"href":734},"\u002Fdocs\u002Freading-the-form\u002Ferrors",[21,736,737],{},"errors",": the variant-filtered per-leaf view vs. the un-filtered aggregate.",[740,741,742],"style",{},"html pre.shiki code .szBVR, html code.shiki .szBVR{--shiki-default:#D73A49;--shiki-dark:#F97583}html pre.shiki code .sVt8B, html code.shiki .sVt8B{--shiki-default:#24292E;--shiki-dark:#E1E4E8}html pre.shiki code .sZZnC, html code.shiki .sZZnC{--shiki-default:#032F62;--shiki-dark:#9ECBFF}html pre.shiki code .sj4cs, html code.shiki .sj4cs{--shiki-default:#005CC5;--shiki-dark:#79B8FF}html pre.shiki code .sScJk, html code.shiki .sScJk{--shiki-default:#6F42C1;--shiki-dark:#B392F0}html .default .shiki span {color: var(--shiki-default);background: var(--shiki-default-bg);font-style: var(--shiki-default-font-style);font-weight: var(--shiki-default-font-weight);text-decoration: var(--shiki-default-text-decoration);}html .shiki span {color: var(--shiki-default);background: var(--shiki-default-bg);font-style: var(--shiki-default-font-style);font-weight: var(--shiki-default-font-weight);text-decoration: var(--shiki-default-text-decoration);}html .dark .shiki span {color: var(--shiki-dark);background: var(--shiki-dark-bg);font-style: var(--shiki-dark-font-style);font-weight: var(--shiki-dark-font-weight);text-decoration: var(--shiki-dark-text-decoration);}html.dark .shiki span {color: var(--shiki-dark);background: var(--shiki-dark-bg);font-style: var(--shiki-dark-font-style);font-weight: var(--shiki-dark-font-weight);text-decoration: var(--shiki-dark-text-decoration);}html pre.shiki code .sJ8bj, html code.shiki .sJ8bj{--shiki-default:#6A737D;--shiki-dark:#6A737D}html pre.shiki code .s9eBZ, html code.shiki .s9eBZ{--shiki-default:#22863A;--shiki-dark:#85E89D}",{"title":50,"searchDepth":75,"depth":75,"links":744},[745,746,747,748,749,750,751],{"id":42,"depth":75,"text":43},{"id":250,"depth":75,"text":251},{"id":390,"depth":75,"text":391},{"id":434,"depth":75,"text":435},{"id":621,"depth":75,"text":622},{"id":696,"depth":75,"text":697},{"id":711,"depth":75,"text":712},"z.discriminatedUnion reshapes storage to the active variant's slim default. Inactive keys purge, new keys seed. The discriminator drives both runtime storage and conditional template rendering.","md",{},[756,759,762,765],{"label":757,"value":758},"Category","Schema feature",{"label":760,"value":761,"kind":21},"Schema","z.discriminatedUnion('key', [variantA, variantB, …])",{"label":763,"value":764},"Reshape trigger","writing the discriminator",{"label":391,"value":766},"on by default (rememberVariants)","\u002Fdocs\u002Fschemas\u002Fdiscriminated-unions",{"title":5,"description":752},null,"docs\u002Fschemas\u002Fdiscriminated-unions","BX2-teTAc-uowHrjReVU8JXKat830RZMc_eHQkzwal4",1780949757734]