[{"data":1,"prerenderedAt":936},["ShallowReactive",2],{"content-\u002Fdocs\u002Fschemas\u002Fvariant-forms":3},{"id":4,"title":5,"body":6,"description":915,"extension":916,"meta":917,"metaRows":918,"navigation":110,"path":931,"seo":932,"source":933,"stem":934,"__hash__":935},"docs\u002Fdocs\u002Fschemas\u002Fvariant-forms.md","Variant forms",{"type":7,"value":8,"toc":906},"minimark",[9,13,34,37,44,48,53,63,254,257,261,267,297,319,389,393,404,689,707,711,714,749,757,761,764,794,801,850,854,864,868,902],[10,11,5],"h1",{"id":12},"variant-forms",[14,15,16],"blockquote",{},[17,18,19,20,24,25,29,30,33],"p",{},"When the form ",[21,22,23],"em",{},"is"," one of several shapes, make the union the root. A ",[26,27,28],"code",{},"z.discriminatedUnion"," schema at the top level gives you a variant form: ",[26,31,32],{},"form.values"," reads the active variant, and writing the discriminator reshapes the whole form.",[35,36],"docs-meta-table",{},[17,38,39,40,43],{},"The demo is a checkout payment method. A payment is exactly one of card, bank transfer, or invoice, each with its own fields, so the schema root is a ",[26,41,42],{},"z.discriminatedUnion('method', …)",". Switch the method and the form reshapes to that variant; the readout below tracks the change.",[45,46],"docs-demo",{"label":47,"slug":12},"Variant Form Demo",[49,50,52],"h2",{"id":51},"the-schema","The schema",[17,54,55,56,58,59,62],{},"A variant form declares a ",[26,57,28],{}," schema as the root, not nested under a key. One discriminator (",[26,60,61],{},"method",") picks the variant; each variant is a regular Zod schema:",[64,65,70],"pre",{"className":66,"code":67,"language":68,"meta":69,"style":69},"language-ts shiki shiki-themes github-light github-dark","import { useForm } from 'attaform\u002Fzod'\nimport { z } from 'zod'\n\nconst schema = z.discriminatedUnion('method', [\n  z.object({ method: z.literal('card'), cardNumber: z.string(), cvc: z.string() }),\n  z.object({ method: z.literal('bank'), iban: z.string() }),\n  z.object({ method: z.literal('invoice'), poNumber: z.string(), netDays: z.number() }),\n])\n\nconst form = useForm({ schema })\n","ts","",[26,71,72,92,105,112,141,175,198,227,233,238],{"__ignoreMap":69},[73,74,77,81,85,88],"span",{"class":75,"line":76},"line",1,[73,78,80],{"class":79},"szBVR","import",[73,82,84],{"class":83},"sVt8B"," { useForm } ",[73,86,87],{"class":79},"from",[73,89,91],{"class":90},"sZZnC"," 'attaform\u002Fzod'\n",[73,93,95,97,100,102],{"class":75,"line":94},2,[73,96,80],{"class":79},[73,98,99],{"class":83}," { z } ",[73,101,87],{"class":79},[73,103,104],{"class":90}," 'zod'\n",[73,106,108],{"class":75,"line":107},3,[73,109,111],{"emptyLinePlaceholder":110},true,"\n",[73,113,115,118,122,125,128,132,135,138],{"class":75,"line":114},4,[73,116,117],{"class":79},"const",[73,119,121],{"class":120},"sj4cs"," schema",[73,123,124],{"class":79}," =",[73,126,127],{"class":83}," z.",[73,129,131],{"class":130},"sScJk","discriminatedUnion",[73,133,134],{"class":83},"(",[73,136,137],{"class":90},"'method'",[73,139,140],{"class":83},", [\n",[73,142,144,147,150,153,156,158,161,164,167,170,172],{"class":75,"line":143},5,[73,145,146],{"class":83},"  z.",[73,148,149],{"class":130},"object",[73,151,152],{"class":83},"({ method: z.",[73,154,155],{"class":130},"literal",[73,157,134],{"class":83},[73,159,160],{"class":90},"'card'",[73,162,163],{"class":83},"), cardNumber: z.",[73,165,166],{"class":130},"string",[73,168,169],{"class":83},"(), cvc: z.",[73,171,166],{"class":130},[73,173,174],{"class":83},"() }),\n",[73,176,178,180,182,184,186,188,191,194,196],{"class":75,"line":177},6,[73,179,146],{"class":83},[73,181,149],{"class":130},[73,183,152],{"class":83},[73,185,155],{"class":130},[73,187,134],{"class":83},[73,189,190],{"class":90},"'bank'",[73,192,193],{"class":83},"), iban: z.",[73,195,166],{"class":130},[73,197,174],{"class":83},[73,199,201,203,205,207,209,211,214,217,219,222,225],{"class":75,"line":200},7,[73,202,146],{"class":83},[73,204,149],{"class":130},[73,206,152],{"class":83},[73,208,155],{"class":130},[73,210,134],{"class":83},[73,212,213],{"class":90},"'invoice'",[73,215,216],{"class":83},"), poNumber: z.",[73,218,166],{"class":130},[73,220,221],{"class":83},"(), netDays: z.",[73,223,224],{"class":130},"number",[73,226,174],{"class":83},[73,228,230],{"class":75,"line":229},8,[73,231,232],{"class":83},"])\n",[73,234,236],{"class":75,"line":235},9,[73,237,111],{"emptyLinePlaceholder":110},[73,239,241,243,246,248,251],{"class":75,"line":240},10,[73,242,117],{"class":79},[73,244,245],{"class":120}," form",[73,247,124],{"class":79},[73,249,250],{"class":130}," useForm",[73,252,253],{"class":83},"({ schema })\n",[17,255,256],{},"There is no wrapper key. The discriminator and every variant's fields sit at the top level of the form.",[49,258,260],{"id":259},"reading-the-active-variant","Reading the active variant",[17,262,263,264,266],{},"Because the union is the root, ",[26,265,32],{}," reads the variant directly. Every variant's keys are reachable at the top level, so a field read works without narrowing first:",[64,268,270],{"className":66,"code":269,"language":68,"meta":69,"style":69},"form.values.method \u002F\u002F the discriminator: string while editing\nform.values.cardNumber \u002F\u002F string | undefined (present on the card variant)\nform.values.iban \u002F\u002F string | undefined (present on the bank variant)\n",[26,271,272,281,289],{"__ignoreMap":69},[73,273,274,277],{"class":75,"line":76},[73,275,276],{"class":83},"form.values.method ",[73,278,280],{"class":279},"sJ8bj","\u002F\u002F the discriminator: string while editing\n",[73,282,283,286],{"class":75,"line":94},[73,284,285],{"class":83},"form.values.cardNumber ",[73,287,288],{"class":279},"\u002F\u002F string | undefined (present on the card variant)\n",[73,290,291,294],{"class":75,"line":107},[73,292,293],{"class":83},"form.values.iban ",[73,295,296],{"class":279},"\u002F\u002F string | undefined (present on the bank variant)\n",[17,298,299,300,303,304,307,308,310,311,318],{},"A per-variant field reads as ",[26,301,302],{},"T | undefined"," because it is present only while its variant is active, matching the runtime, where reading a key from another variant returns ",[26,305,306],{},"undefined",". The discriminator reads as ",[26,309,166],{}," while editing, since the user may be mid-switch. Inside ",[312,313,315],"a",{"href":314},"\u002Fdocs\u002Fsubmitting\u002Fhandle-submit",[26,316,317],{},"handleSubmit"," the parsed value is the true narrowed union, so you branch on it with full type narrowing:",[64,320,322],{"className":66,"code":321,"language":68,"meta":69,"style":69},"const onSubmit = form.handleSubmit((payment) => {\n  if (payment.method === 'card') {\n    payment.cardNumber \u002F\u002F string, narrowed to the card variant\n  }\n})\n",[26,323,324,354,371,379,384],{"__ignoreMap":69},[73,325,326,328,331,333,336,338,341,345,348,351],{"class":75,"line":76},[73,327,117],{"class":79},[73,329,330],{"class":120}," onSubmit",[73,332,124],{"class":79},[73,334,335],{"class":83}," form.",[73,337,317],{"class":130},[73,339,340],{"class":83},"((",[73,342,344],{"class":343},"s4XuR","payment",[73,346,347],{"class":83},") ",[73,349,350],{"class":79},"=>",[73,352,353],{"class":83}," {\n",[73,355,356,359,362,365,368],{"class":75,"line":94},[73,357,358],{"class":79},"  if",[73,360,361],{"class":83}," (payment.method ",[73,363,364],{"class":79},"===",[73,366,367],{"class":90}," 'card'",[73,369,370],{"class":83},") {\n",[73,372,373,376],{"class":75,"line":107},[73,374,375],{"class":83},"    payment.cardNumber ",[73,377,378],{"class":279},"\u002F\u002F string, narrowed to the card variant\n",[73,380,381],{"class":75,"line":114},[73,382,383],{"class":83},"  }\n",[73,385,386],{"class":75,"line":143},[73,387,388],{"class":83},"})\n",[49,390,392],{"id":391},"binding-the-discriminator-and-variant-fields","Binding the discriminator and variant fields",[17,394,395,396,399,400,403],{},"The discriminator is an ordinary field: bind it to a ",[26,397,398],{},"\u003Cselect>"," or a set of radios, and each variant's fields bind by their own key, no wrapper prefix. Branch the template on ",[26,401,402],{},"form.values.method",":",[64,405,409],{"className":406,"code":407,"language":408,"meta":69,"style":69},"language-vue shiki shiki-themes github-light github-dark","\u003Ctemplate>\n  \u003Clabel>\n    Payment method\n    \u003Cselect v-register=\"form.register('method')\">\n      \u003Coption value=\"card\">Card\u003C\u002Foption>\n      \u003Coption value=\"bank\">Bank transfer\u003C\u002Foption>\n      \u003Coption value=\"invoice\">Invoice\u003C\u002Foption>\n    \u003C\u002Fselect>\n  \u003C\u002Flabel>\n\n  \u003Ctemplate v-if=\"form.values.method === 'card'\">\n    \u003Cinput v-register=\"form.register('cardNumber')\" \u002F>\n    \u003Cem v-if=\"form.fields.cardNumber?.showErrors\">{{\n      form.fields.cardNumber?.firstError?.message\n    }}\u003C\u002Fem>\n  \u003C\u002Ftemplate>\n\n  \u003Ctemplate v-else-if=\"form.values.method === 'bank'\">\n    \u003Cinput v-register=\"form.register('iban')\" \u002F>\n  \u003C\u002Ftemplate>\n\u003C\u002Ftemplate>\n","vue",[26,410,411,423,433,438,457,480,500,520,529,538,542,566,584,601,607,617,626,631,654,670,679],{"__ignoreMap":69},[73,412,413,416,420],{"class":75,"line":76},[73,414,415],{"class":83},"\u003C",[73,417,419],{"class":418},"s9eBZ","template",[73,421,422],{"class":83},">\n",[73,424,425,428,431],{"class":75,"line":94},[73,426,427],{"class":83},"  \u003C",[73,429,430],{"class":418},"label",[73,432,422],{"class":83},[73,434,435],{"class":75,"line":107},[73,436,437],{"class":83},"    Payment method\n",[73,439,440,443,446,449,452,455],{"class":75,"line":114},[73,441,442],{"class":83},"    \u003C",[73,444,445],{"class":418},"select",[73,447,448],{"class":130}," v-register",[73,450,451],{"class":83},"=",[73,453,454],{"class":90},"\"form.register('method')\"",[73,456,422],{"class":83},[73,458,459,462,465,468,470,473,476,478],{"class":75,"line":143},[73,460,461],{"class":83},"      \u003C",[73,463,464],{"class":418},"option",[73,466,467],{"class":130}," value",[73,469,451],{"class":83},[73,471,472],{"class":90},"\"card\"",[73,474,475],{"class":83},">Card\u003C\u002F",[73,477,464],{"class":418},[73,479,422],{"class":83},[73,481,482,484,486,488,490,493,496,498],{"class":75,"line":177},[73,483,461],{"class":83},[73,485,464],{"class":418},[73,487,467],{"class":130},[73,489,451],{"class":83},[73,491,492],{"class":90},"\"bank\"",[73,494,495],{"class":83},">Bank transfer\u003C\u002F",[73,497,464],{"class":418},[73,499,422],{"class":83},[73,501,502,504,506,508,510,513,516,518],{"class":75,"line":200},[73,503,461],{"class":83},[73,505,464],{"class":418},[73,507,467],{"class":130},[73,509,451],{"class":83},[73,511,512],{"class":90},"\"invoice\"",[73,514,515],{"class":83},">Invoice\u003C\u002F",[73,517,464],{"class":418},[73,519,422],{"class":83},[73,521,522,525,527],{"class":75,"line":229},[73,523,524],{"class":83},"    \u003C\u002F",[73,526,445],{"class":418},[73,528,422],{"class":83},[73,530,531,534,536],{"class":75,"line":235},[73,532,533],{"class":83},"  \u003C\u002F",[73,535,430],{"class":418},[73,537,422],{"class":83},[73,539,540],{"class":75,"line":240},[73,541,111],{"emptyLinePlaceholder":110},[73,543,545,547,549,552,554,557,559,561,564],{"class":75,"line":544},11,[73,546,427],{"class":83},[73,548,419],{"class":418},[73,550,551],{"class":79}," v-if",[73,553,451],{"class":83},[73,555,556],{"class":90},"\"",[73,558,276],{"class":83},[73,560,364],{"class":79},[73,562,563],{"class":90}," 'card'\"",[73,565,422],{"class":83},[73,567,569,571,574,576,578,581],{"class":75,"line":568},12,[73,570,442],{"class":83},[73,572,573],{"class":418},"input",[73,575,448],{"class":130},[73,577,451],{"class":83},[73,579,580],{"class":90},"\"form.register('cardNumber')\"",[73,582,583],{"class":83}," \u002F>\n",[73,585,587,589,591,593,595,598],{"class":75,"line":586},13,[73,588,442],{"class":83},[73,590,21],{"class":418},[73,592,551],{"class":130},[73,594,451],{"class":83},[73,596,597],{"class":90},"\"form.fields.cardNumber?.showErrors\"",[73,599,600],{"class":83},">{{\n",[73,602,604],{"class":75,"line":603},14,[73,605,606],{"class":83},"      form.fields.cardNumber?.firstError?.message\n",[73,608,610,613,615],{"class":75,"line":609},15,[73,611,612],{"class":83},"    }}\u003C\u002F",[73,614,21],{"class":418},[73,616,422],{"class":83},[73,618,620,622,624],{"class":75,"line":619},16,[73,621,533],{"class":83},[73,623,419],{"class":418},[73,625,422],{"class":83},[73,627,629],{"class":75,"line":628},17,[73,630,111],{"emptyLinePlaceholder":110},[73,632,634,636,638,641,643,645,647,649,652],{"class":75,"line":633},18,[73,635,427],{"class":83},[73,637,419],{"class":418},[73,639,640],{"class":79}," v-else-if",[73,642,451],{"class":83},[73,644,556],{"class":90},[73,646,276],{"class":83},[73,648,364],{"class":79},[73,650,651],{"class":90}," 'bank'\"",[73,653,422],{"class":83},[73,655,657,659,661,663,665,668],{"class":75,"line":656},19,[73,658,442],{"class":83},[73,660,573],{"class":418},[73,662,448],{"class":130},[73,664,451],{"class":83},[73,666,667],{"class":90},"\"form.register('iban')\"",[73,669,583],{"class":83},[73,671,673,675,677],{"class":75,"line":672},20,[73,674,533],{"class":83},[73,676,419],{"class":418},[73,678,422],{"class":83},[73,680,682,685,687],{"class":75,"line":681},21,[73,683,684],{"class":83},"\u003C\u002F",[73,686,419],{"class":418},[73,688,422],{"class":83},[17,690,691,692,695,696,699,700,706],{},"A variant field's node is absent while its variant is inactive, so reach it through ",[26,693,694],{},"?.",": ",[26,697,698],{},"form.fields.cardNumber?.showErrors",". The same chaining applies to ",[312,701,703],{"href":702},"\u002Fdocs\u002Freading-the-form\u002Ferrors",[26,704,705],{},"form.errors",", which is variant-filtered, so an inactive variant's errors stay silent.",[49,708,710],{"id":709},"switching-variants-reshapes-the-form","Switching variants reshapes the form",[17,712,713],{},"Writing the discriminator reshapes storage to the new variant: the outgoing variant's keys are purged, and the new variant's keys seed from the schema's slim defaults. At the root, the reshape is the whole form:",[64,715,717],{"className":66,"code":716,"language":68,"meta":69,"style":69},"form.setValue('method', 'bank')\n\u002F\u002F form.values() : { method: 'bank', iban: '' }\n\u002F\u002F   (card's cardNumber \u002F cvc purged; bank's iban seeded)\n",[26,718,719,739,744],{"__ignoreMap":69},[73,720,721,724,727,729,731,734,736],{"class":75,"line":76},[73,722,723],{"class":83},"form.",[73,725,726],{"class":130},"setValue",[73,728,134],{"class":83},[73,730,137],{"class":90},[73,732,733],{"class":83},", ",[73,735,190],{"class":90},[73,737,738],{"class":83},")\n",[73,740,741],{"class":75,"line":94},[73,742,743],{"class":279},"\u002F\u002F form.values() : { method: 'bank', iban: '' }\n",[73,745,746],{"class":75,"line":107},[73,747,748],{"class":279},"\u002F\u002F   (card's cardNumber \u002F cvc purged; bank's iban seeded)\n",[17,750,751,752,756],{},"This is the same engine that drives a nested discriminated union, run at the root path. Variant memory (restoring a variant's values when you switch back to it) is on by default. The full reshape and memory semantics live on ",[312,753,755],{"href":754},"\u002Fdocs\u002Fschemas\u002Fdiscriminated-unions","Discriminated unions",", which covers the same feature as a field inside an object form.",[49,758,760],{"id":759},"defaults","Defaults",[17,762,763],{},"A bare variant root defaults to the first variant, with its fields at their slim defaults:",[64,765,767],{"className":66,"code":766,"language":68,"meta":69,"style":69},"const form = useForm({ schema })\nform.values() \u002F\u002F { method: 'card', cardNumber: '', cvc: '' }\n",[26,768,769,781],{"__ignoreMap":69},[73,770,771,773,775,777,779],{"class":75,"line":76},[73,772,117],{"class":79},[73,774,245],{"class":120},[73,776,124],{"class":79},[73,778,250],{"class":130},[73,780,253],{"class":83},[73,782,783,785,788,791],{"class":75,"line":94},[73,784,723],{"class":83},[73,786,787],{"class":130},"values",[73,789,790],{"class":83},"() ",[73,792,793],{"class":279},"\u002F\u002F { method: 'card', cardNumber: '', cvc: '' }\n",[17,795,796,797,800],{},"Start on a different variant, or seed its fields, with ",[26,798,799],{},"defaultValues",". Pass one complete variant:",[64,802,804],{"className":66,"code":803,"language":68,"meta":69,"style":69},"const form = useForm({\n  schema,\n  defaultValues: { method: 'invoice', poNumber: 'PO-1', netDays: 30 },\n})\n",[26,805,806,819,824,846],{"__ignoreMap":69},[73,807,808,810,812,814,816],{"class":75,"line":76},[73,809,117],{"class":79},[73,811,245],{"class":120},[73,813,124],{"class":79},[73,815,250],{"class":130},[73,817,818],{"class":83},"({\n",[73,820,821],{"class":75,"line":94},[73,822,823],{"class":83},"  schema,\n",[73,825,826,829,831,834,837,840,843],{"class":75,"line":107},[73,827,828],{"class":83},"  defaultValues: { method: ",[73,830,213],{"class":90},[73,832,833],{"class":83},", poNumber: ",[73,835,836],{"class":90},"'PO-1'",[73,838,839],{"class":83},", netDays: ",[73,841,842],{"class":120},"30",[73,844,845],{"class":83}," },\n",[73,847,848],{"class":75,"line":114},[73,849,388],{"class":83},[49,851,853],{"id":852},"when-the-shape-is-fixed-reach-for-an-object","When the shape is fixed, reach for an object",[17,855,856,857,860,861,863],{},"Variant forms are for a value that is genuinely one of several shapes. When every field is always present, a ",[26,858,859],{},"z.object"," root is the better fit. The two compose freely: an object form can hold a discriminated-union field (see ",[312,862,755],{"href":754},"), and a variant's schema can be any Zod schema, including nested objects, arrays, and records.",[49,865,867],{"id":866},"where-to-next","Where to next",[869,870,871,877,884,895],"ul",{},[872,873,874,876],"li",{},[312,875,755],{"href":754},": the same feature as a field inside an object form, with the full reshape and error-filtering detail.",[872,878,879,883],{},[312,880,882],{"href":881},"\u002Fdocs\u002Fwriting-and-mutating\u002Fvariant-memory","Variant memory",": when to keep a variant's values on switch-back, when to drop them.",[872,885,886,890,891,894],{},[312,887,889],{"href":888},"\u002Fdocs\u002Fschemas\u002Fdictionary-forms","Dictionary forms",": the other non-object root, a ",[26,892,893],{},"z.record"," map.",[872,896,897,901],{},[312,898,899],{"href":314},[26,900,317],{},": where the parsed value narrows to the active variant.",[903,904,905],"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 .s4XuR, html code.shiki .s4XuR{--shiki-default:#E36209;--shiki-dark:#FFAB70}html pre.shiki code .s9eBZ, html code.shiki .s9eBZ{--shiki-default:#22863A;--shiki-dark:#85E89D}",{"title":69,"searchDepth":94,"depth":94,"links":907},[908,909,910,911,912,913,914],{"id":51,"depth":94,"text":52},{"id":259,"depth":94,"text":260},{"id":391,"depth":94,"text":392},{"id":709,"depth":94,"text":710},{"id":759,"depth":94,"text":760},{"id":852,"depth":94,"text":853},{"id":866,"depth":94,"text":867},"A z.discriminatedUnion schema can be the form root, not just a field. form.values reads the active variant at the top level, register binds variant fields by their own key, and a bare root defaults to the first variant.","md",{},[919,922,925,928],{"label":920,"value":921},"Category","Schema feature",{"label":923,"value":924,"kind":26},"Form root","z.discriminatedUnion('key', [variantA, variantB, …])",{"label":926,"value":927},"Reshape trigger","writing the discriminator",{"label":929,"value":930},"Default","first variant","\u002Fdocs\u002Fschemas\u002Fvariant-forms",{"title":5,"description":915},null,"docs\u002Fschemas\u002Fvariant-forms","BAIubveppUhxkK7u04mRwzmMzpt6oorch6Vyhz6tKck",1781538315787]