[{"data":1,"prerenderedAt":742},["ShallowReactive",2],{"content-\u002Fdocs\u002Fbinding-inputs\u002Fcoercion":3},{"id":4,"title":5,"body":6,"description":722,"extension":723,"meta":724,"metaRows":725,"navigation":368,"path":737,"seo":738,"source":739,"stem":740,"__hash__":741},"docs\u002Fdocs\u002Fbinding-inputs\u002Fcoercion.md","Schema-driven coercion",{"type":7,"value":8,"toc":714},"minimark",[9,13,20,23,55,60,65,130,178,193,197,219,222,230,236,249,253,259,322,333,537,548,552,555,640,649,653,664,668,710],[10,11,5],"h1",{"id":12},"schema-driven-coercion",[14,15,16],"blockquote",{},[17,18,19],"p",{},"Two built-in rules handle 90 % of real forms: string → number and string → boolean. Compose more when your schema needs them.",[21,22],"docs-meta-table",{},[17,24,25,26,30,31,34,35,38,39,42,43,46,47,50,51,54],{},"Type a number into the ",[27,28,29],"code",{},"count"," field and watch it land in storage as a ",[27,32,33],{},"number"," (not a string). Type \"true\" or \"false\" into ",[27,36,37],{},"enabled"," and see it commit as a ",[27,40,41],{},"boolean",". Both inputs are ",[27,44,45],{},"type=\"text\"","; coercion is what makes ",[27,48,49],{},"z.number()"," and ",[27,52,53],{},"z.boolean()"," leaves work against plain text inputs at the directive layer.",[56,57],"docs-demo",{"label":58,"slug":59},"Coercion Demo","coercion",[61,62,64],"h2",{"id":63},"the-default-registry","The default registry",[66,67,72],"pre",{"className":68,"code":69,"language":70,"meta":71,"style":71},"language-ts shiki shiki-themes github-light github-dark","defaultCoercionRules = [\n  { input: 'string', output: 'number', transform: parseNumber },\n  { input: 'string', output: 'boolean', transform: parseTrueFalse },\n]\n","ts","",[27,73,74,90,109,124],{"__ignoreMap":71},[75,76,79,83,87],"span",{"class":77,"line":78},"line",1,[75,80,82],{"class":81},"sVt8B","defaultCoercionRules ",[75,84,86],{"class":85},"szBVR","=",[75,88,89],{"class":81}," [\n",[75,91,93,96,100,103,106],{"class":77,"line":92},2,[75,94,95],{"class":81},"  { input: ",[75,97,99],{"class":98},"sZZnC","'string'",[75,101,102],{"class":81},", output: ",[75,104,105],{"class":98},"'number'",[75,107,108],{"class":81},", transform: parseNumber },\n",[75,110,112,114,116,118,121],{"class":77,"line":111},3,[75,113,95],{"class":81},[75,115,99],{"class":98},[75,117,102],{"class":81},[75,119,120],{"class":98},"'boolean'",[75,122,123],{"class":81},", transform: parseTrueFalse },\n",[75,125,127],{"class":77,"line":126},4,[75,128,129],{"class":81},"]\n",[131,132,133,153],"ul",{},[134,135,136,140,141,144,145,148,149,152],"li",{},[137,138,139],"strong",{},"string → number"," trims whitespace, parses with ",[27,142,143],{},"Number()",", returns ",[27,146,147],{},"coerced: false"," on ",[27,150,151],{},"NaN"," (the slim gate then rejects the unparseable value with a friendly message). Whitespace-only inputs skip the coercion so blank-paths machinery stays in charge.",[134,154,155,158,159,162,163,166,167,170,171,170,174,177],{},[137,156,157],{},"string → boolean"," lowercases + trims, accepts ",[27,160,161],{},"'true'"," \u002F ",[27,164,165],{},"'false'"," in any case (",[27,168,169],{},"'True'",", ",[27,172,173],{},"'FALSE'",[27,175,176],{},"' true '","). Anything else skips so the gate can reject.",[17,179,180,181,184,185,188,189,192],{},"The same two rules cover most native HTML input shapes: ",[27,182,183],{},"\u003Cinput type=\"number\">"," (number leaf), ",[27,186,187],{},"\u003Cinput type=\"checkbox\" value=\"...\">"," (boolean leaf), ",[27,190,191],{},"\u003Cselect>"," with numeric option values.",[61,194,196],{"id":195},"when-coercion-fires","When coercion fires",[17,198,199,200,203,204,170,211,214,215,218],{},"Coercion runs ",[137,201,202],{},"only on user-typed DOM values",". Programmatic writes through ",[205,206,208],"a",{"href":207},"\u002Fdocs\u002Fwriting-and-mutating\u002Fset-value",[27,209,210],{},"form.setValue",[27,212,213],{},"form.register('path').setValueWithInternalPath",", or the field-array helpers are ",[137,216,217],{},"never"," coerced; they're typed against the schema's leaf type at the call site, so the value already matches. The strictness is intentional: if you've got the value in hand in code, you knew its type when you typed it.",[17,220,221],{},"The coercion step sits between the directive's value extraction and the slim-type gate's write check:",[66,223,228],{"className":224,"code":226,"language":227},[225],"language-text","DOM event → extract → modifier (.trim, .number) → transforms[] → coerce → slim gate → storage\n","text",[27,229,226],{"__ignoreMap":71},[17,231,232,233,235],{},"A value the registry can't coerce (",[27,234,147],{},") passes through unchanged; the slim gate handles the rejection downstream with a typed diagnostic.",[17,237,238,244,245,248],{},[205,239,241],{"href":240},"\u002Fdocs\u002Fbinding-inputs\u002Ffile",[27,242,243],{},"\u003Cinput type=\"file\">"," inputs skip coercion entirely; ",[27,246,247],{},"File"," handles are objects, not strings, and land in storage as-is.",[61,250,252],{"id":251},"extending-the-default-registry","Extending the default registry",[17,254,255,258],{},[27,256,257],{},"useForm({ coerce })"," accepts three forms:",[66,260,262],{"className":68,"code":261,"language":70,"meta":71,"style":71},"useForm({ coerce: true }) \u002F\u002F   defaults (string→number, string→boolean)\nuseForm({ coerce: false }) \u002F\u002F   no coercion; slim gate rejects mismatches as-is\nuseForm({ coerce: [...defaultCoercionRules, defineCoercion({ ... })] })\n",[27,263,264,284,298],{"__ignoreMap":71},[75,265,266,270,273,277,280],{"class":77,"line":78},[75,267,269],{"class":268},"sScJk","useForm",[75,271,272],{"class":81},"({ coerce: ",[75,274,276],{"class":275},"sj4cs","true",[75,278,279],{"class":81}," }) ",[75,281,283],{"class":282},"sJ8bj","\u002F\u002F   defaults (string→number, string→boolean)\n",[75,285,286,288,290,293,295],{"class":77,"line":92},[75,287,269],{"class":268},[75,289,272],{"class":81},[75,291,292],{"class":275},"false",[75,294,279],{"class":81},[75,296,297],{"class":282},"\u002F\u002F   no coercion; slim gate rejects mismatches as-is\n",[75,299,300,302,305,308,311,314,317,319],{"class":77,"line":111},[75,301,269],{"class":268},[75,303,304],{"class":81},"({ coerce: [",[75,306,307],{"class":85},"...",[75,309,310],{"class":81},"defaultCoercionRules, ",[75,312,313],{"class":268},"defineCoercion",[75,315,316],{"class":81},"({ ",[75,318,307],{"class":85},[75,320,321],{"class":81}," })] })\n",[17,323,324,325,328,329,332],{},"Spread ",[27,326,327],{},"defaultCoercionRules"," to extend; pass a bare array to ",[137,330,331],{},"replace"," entirely. Adding a string → Date rule for ISO timestamps:",[66,334,336],{"className":68,"code":335,"language":70,"meta":71,"style":71},"import { useForm } from 'attaform\u002Fzod'\nimport { defaultCoercionRules, defineCoercion } from 'attaform'\n\nuseForm({\n  schema,\n  coerce: [\n    ...defaultCoercionRules,\n    defineCoercion({\n      input: 'string',\n      output: 'date',\n      transform: (s) => {\n        const d = new Date(s)\n        return Number.isFinite(d.getTime()) ? { coerced: true, value: d } : { coerced: false }\n      },\n    }),\n  ],\n})\n",[27,337,338,352,364,370,377,383,389,398,406,417,428,450,471,513,519,525,531],{"__ignoreMap":71},[75,339,340,343,346,349],{"class":77,"line":78},[75,341,342],{"class":85},"import",[75,344,345],{"class":81}," { useForm } ",[75,347,348],{"class":85},"from",[75,350,351],{"class":98}," 'attaform\u002Fzod'\n",[75,353,354,356,359,361],{"class":77,"line":92},[75,355,342],{"class":85},[75,357,358],{"class":81}," { defaultCoercionRules, defineCoercion } ",[75,360,348],{"class":85},[75,362,363],{"class":98}," 'attaform'\n",[75,365,366],{"class":77,"line":111},[75,367,369],{"emptyLinePlaceholder":368},true,"\n",[75,371,372,374],{"class":77,"line":126},[75,373,269],{"class":268},[75,375,376],{"class":81},"({\n",[75,378,380],{"class":77,"line":379},5,[75,381,382],{"class":81},"  schema,\n",[75,384,386],{"class":77,"line":385},6,[75,387,388],{"class":81},"  coerce: [\n",[75,390,392,395],{"class":77,"line":391},7,[75,393,394],{"class":85},"    ...",[75,396,397],{"class":81},"defaultCoercionRules,\n",[75,399,401,404],{"class":77,"line":400},8,[75,402,403],{"class":268},"    defineCoercion",[75,405,376],{"class":81},[75,407,409,412,414],{"class":77,"line":408},9,[75,410,411],{"class":81},"      input: ",[75,413,99],{"class":98},[75,415,416],{"class":81},",\n",[75,418,420,423,426],{"class":77,"line":419},10,[75,421,422],{"class":81},"      output: ",[75,424,425],{"class":98},"'date'",[75,427,416],{"class":81},[75,429,431,434,437,441,444,447],{"class":77,"line":430},11,[75,432,433],{"class":268},"      transform",[75,435,436],{"class":81},": (",[75,438,440],{"class":439},"s4XuR","s",[75,442,443],{"class":81},") ",[75,445,446],{"class":85},"=>",[75,448,449],{"class":81}," {\n",[75,451,453,456,459,462,465,468],{"class":77,"line":452},12,[75,454,455],{"class":85},"        const",[75,457,458],{"class":275}," d",[75,460,461],{"class":85}," =",[75,463,464],{"class":85}," new",[75,466,467],{"class":268}," Date",[75,469,470],{"class":81},"(s)\n",[75,472,474,477,480,483,486,489,492,495,498,500,503,506,508,510],{"class":77,"line":473},13,[75,475,476],{"class":85},"        return",[75,478,479],{"class":81}," Number.",[75,481,482],{"class":268},"isFinite",[75,484,485],{"class":81},"(d.",[75,487,488],{"class":268},"getTime",[75,490,491],{"class":81},"()) ",[75,493,494],{"class":85},"?",[75,496,497],{"class":81}," { coerced: ",[75,499,276],{"class":275},[75,501,502],{"class":81},", value: d } ",[75,504,505],{"class":85},":",[75,507,497],{"class":81},[75,509,292],{"class":275},[75,511,512],{"class":81}," }\n",[75,514,516],{"class":77,"line":515},14,[75,517,518],{"class":81},"      },\n",[75,520,522],{"class":77,"line":521},15,[75,523,524],{"class":81},"    }),\n",[75,526,528],{"class":77,"line":527},16,[75,529,530],{"class":81},"  ],\n",[75,532,534],{"class":77,"line":533},17,[75,535,536],{"class":81},"})\n",[17,538,539,540,543,544,547],{},"Now ",[27,541,542],{},"\u003Cinput type=\"text\" v-register=\"form.register('publishedAt')\" \u002F>"," against a ",[27,545,546],{},"z.date()"," leaf works without modifiers.",[61,549,551],{"id":550},"app-wide-defaults","App-wide defaults",[17,553,554],{},"Set coercion at the plugin level so every form picks up the same custom rule without per-form opt-in:",[66,556,558],{"className":68,"code":557,"language":70,"meta":71,"style":71},"import { createAttaform } from 'attaform\u002Fzod'\nimport { defaultCoercionRules, defineCoercion } from 'attaform'\n\ncreateAttaform({\n  defaults: {\n    coerce: [\n      ...defaultCoercionRules,\n      defineCoercion({\n        \u002F* ... *\u002F\n      }),\n    ],\n  },\n})\n",[27,559,560,571,581,585,592,597,602,609,616,621,626,631,636],{"__ignoreMap":71},[75,561,562,564,567,569],{"class":77,"line":78},[75,563,342],{"class":85},[75,565,566],{"class":81}," { createAttaform } ",[75,568,348],{"class":85},[75,570,351],{"class":98},[75,572,573,575,577,579],{"class":77,"line":92},[75,574,342],{"class":85},[75,576,358],{"class":81},[75,578,348],{"class":85},[75,580,363],{"class":98},[75,582,583],{"class":77,"line":111},[75,584,369],{"emptyLinePlaceholder":368},[75,586,587,590],{"class":77,"line":126},[75,588,589],{"class":268},"createAttaform",[75,591,376],{"class":81},[75,593,594],{"class":77,"line":379},[75,595,596],{"class":81},"  defaults: {\n",[75,598,599],{"class":77,"line":385},[75,600,601],{"class":81},"    coerce: [\n",[75,603,604,607],{"class":77,"line":391},[75,605,606],{"class":85},"      ...",[75,608,397],{"class":81},[75,610,611,614],{"class":77,"line":400},[75,612,613],{"class":268},"      defineCoercion",[75,615,376],{"class":81},[75,617,618],{"class":77,"line":408},[75,619,620],{"class":282},"        \u002F* ... *\u002F\n",[75,622,623],{"class":77,"line":419},[75,624,625],{"class":81},"      }),\n",[75,627,628],{"class":77,"line":430},[75,629,630],{"class":81},"    ],\n",[75,632,633],{"class":77,"line":452},[75,634,635],{"class":81},"  },\n",[75,637,638],{"class":77,"line":473},[75,639,536],{"class":81},[17,641,642,643,645,646,648],{},"Per-form ",[27,644,257],{}," overrides the plugin default. The plugin default overrides Attaform's built-in default (",[27,647,327],{},"). Three layers, deterministic resolution.",[61,650,652],{"id":651},"sync-no-throws","Sync, no throws",[17,654,655,656,659,660,663],{},"Coercion rules MUST be sync. They SHOULD NOT throw; wrap internal try \u002F catch when the conversion can fail (e.g. ",[27,657,658],{},"BigInt('not-a-number')"," throws for non-numeric strings). Attaform wraps each invocation in try \u002F catch as defense in depth; throws are caught, logged once per ",[27,661,662],{},"(input, output)"," pair, and the original value passes through to the slim gate.",[61,665,667],{"id":666},"where-to-next","Where to next",[131,669,670,685,692,699],{},[134,671,672,676,677,680,681,684],{},[205,673,675],{"href":674},"\u002Fdocs\u002Fbinding-inputs\u002Fmodifiers","Modifiers",": ",[27,678,679],{},".number"," for the ",[27,682,683],{},"\u003Cinput type=\"text\">"," + numeric-leaf combo without touching the registry.",[134,686,687,691],{},[205,688,690],{"href":689},"\u002Fdocs\u002Fbinding-inputs\u002Ftransforms","Register transforms",": the per-field write pipeline that runs before coercion.",[134,693,694,698],{},[205,695,696],{"href":207},[27,697,210],{},": the programmatic-write surface that bypasses coercion entirely.",[134,700,701,709],{},[205,702,704,705,708],{"href":703},"\u002Fdocs\u002Fbinding-inputs\u002Fv-register","The ",[27,706,707],{},"v-register"," directive",": the layer coercion plugs into.",[711,712,713],"style",{},"html pre.shiki code .sVt8B, html code.shiki .sVt8B{--shiki-default:#24292E;--shiki-dark:#E1E4E8}html pre.shiki code .szBVR, html code.shiki .szBVR{--shiki-default:#D73A49;--shiki-dark:#F97583}html pre.shiki code .sZZnC, html code.shiki .sZZnC{--shiki-default:#032F62;--shiki-dark:#9ECBFF}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 .sScJk, html code.shiki .sScJk{--shiki-default:#6F42C1;--shiki-dark:#B392F0}html pre.shiki code .sj4cs, html code.shiki .sj4cs{--shiki-default:#005CC5;--shiki-dark:#79B8FF}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}",{"title":71,"searchDepth":92,"depth":92,"links":715},[716,717,718,719,720,721],{"id":63,"depth":92,"text":64},{"id":195,"depth":92,"text":196},{"id":251,"depth":92,"text":252},{"id":550,"depth":92,"text":551},{"id":651,"depth":92,"text":652},{"id":666,"depth":92,"text":667},"The default coercion registry turns DOM strings into numbers and booleans automatically. Compose your own entries to coerce any leaf type.","md",{},[726,729,732,734],{"label":727,"value":728},"Category","Directive layer",{"label":730,"value":731},"Defaults","string → number · string → boolean",{"label":733,"value":257,"kind":27},"Option",{"label":735,"value":736,"kind":27},"Registry type","readonly CoercionEntry[]","\u002Fdocs\u002Fbinding-inputs\u002Fcoercion",{"title":5,"description":722},null,"docs\u002Fbinding-inputs\u002Fcoercion","e1CbG_cCXHUY-oPbnmyFMmHlwzc5maqrPmNTEhmM9Rk",1780949759559]