[{"data":1,"prerenderedAt":748},["ShallowReactive",2],{"content-\u002Fdocs\u002Frecipes\u002Fcoerce":3},{"id":4,"title":5,"body":6,"description":741,"extension":742,"meta":743,"navigation":147,"path":744,"seo":745,"stem":746,"__hash__":747},"docs\u002Fdocs\u002Frecipes\u002Fcoerce.md","Schema-driven coercion",{"type":7,"value":8,"toc":731},"minimark",[9,13,45,72,77,165,253,257,276,279,283,296,551,558,562,565,582,585,589,592,618,625,629,632,640,659,663,706,710,717,727],[10,11,5],"h1",{"id":12},"schema-driven-coercion",[14,15,16,17,21,22,25,26,29,30,33,34,25,37,40,41,44],"p",{},"Attaform coerces user-typed DOM values to the schema's slim type at the\ndirective layer — ",[18,19,20],"code",{},"'25'"," → ",[18,23,24],{},"25"," for a ",[18,27,28],{},"z.number()"," slot, ",[18,31,32],{},"'true'"," →\n",[18,35,36],{},"true",[18,38,39],{},"z.boolean()"," slot. The schema is authoritative for\nstorage shape; consumers stop sprinkling ",[18,42,43],{},".number"," modifiers across\ntemplates.",[14,46,47,48,52,53,56,57,60,61,64,65,60,68,71],{},"Coercion is ",[49,50,51],"strong",{},"on by default"," with the built-in ",[18,54,55],{},"defaultCoercionRules","\n(",[18,58,59],{},"string→number",", ",[18,62,63],{},"string→boolean","). Programmatic writes\n(",[18,66,67],{},"form.setValue",[18,69,70],{},"setValueWithInternalPath",") are NEVER coerced —\ncoercion is user-input-only.",[73,74,76],"h2",{"id":75},"default-in-action","Default in action",[78,79,84],"pre",{"className":80,"code":81,"language":82,"meta":83,"style":83},"language-ts shiki shiki-themes github-light github-dark","const schema = z.object({\n  age: z.number(),\n  isAdmin: z.boolean(),\n})\n\nconst form = useForm({ schema })\n","ts","",[18,85,86,113,125,136,142,149],{"__ignoreMap":83},[87,88,91,95,99,102,106,110],"span",{"class":89,"line":90},"line",1,[87,92,94],{"class":93},"szBVR","const",[87,96,98],{"class":97},"sj4cs"," schema",[87,100,101],{"class":93}," =",[87,103,105],{"class":104},"sVt8B"," z.",[87,107,109],{"class":108},"sScJk","object",[87,111,112],{"class":104},"({\n",[87,114,116,119,122],{"class":89,"line":115},2,[87,117,118],{"class":104},"  age: z.",[87,120,121],{"class":108},"number",[87,123,124],{"class":104},"(),\n",[87,126,128,131,134],{"class":89,"line":127},3,[87,129,130],{"class":104},"  isAdmin: z.",[87,132,133],{"class":108},"boolean",[87,135,124],{"class":104},[87,137,139],{"class":89,"line":138},4,[87,140,141],{"class":104},"})\n",[87,143,145],{"class":89,"line":144},5,[87,146,148],{"emptyLinePlaceholder":147},true,"\n",[87,150,152,154,157,159,162],{"class":89,"line":151},6,[87,153,94],{"class":93},[87,155,156],{"class":97}," form",[87,158,101],{"class":93},[87,160,161],{"class":108}," useForm",[87,163,164],{"class":104},"({ schema })\n",[78,166,170],{"className":167,"code":168,"language":169,"meta":83,"style":83},"language-vue shiki shiki-themes github-light github-dark","\u003Cinput v-register=\"form.register('age')\" \u002F>\n\u003C!-- type \"25\", form.values.age === 25 (a number) -->\n\n\u003Cinput v-register=\"form.register('isAdmin')\" \u002F>\n\u003C!-- type \"true\", form.values.isAdmin === true (a boolean) -->\n","vue",[18,171,172,211,217,221,248],{"__ignoreMap":83},[87,173,174,177,181,184,187,191,194,197,200,203,206,208],{"class":89,"line":90},[87,175,176],{"class":104},"\u003C",[87,178,180],{"class":179},"s9eBZ","input",[87,182,183],{"class":108}," v-register",[87,185,186],{"class":104},"=",[87,188,190],{"class":189},"sZZnC","\"",[87,192,193],{"class":104},"form.",[87,195,196],{"class":108},"register",[87,198,199],{"class":104},"(",[87,201,202],{"class":189},"'age'",[87,204,205],{"class":104},")",[87,207,190],{"class":189},[87,209,210],{"class":104}," \u002F>\n",[87,212,213],{"class":89,"line":115},[87,214,216],{"class":215},"sJ8bj","\u003C!-- type \"25\", form.values.age === 25 (a number) -->\n",[87,218,219],{"class":89,"line":127},[87,220,148],{"emptyLinePlaceholder":147},[87,222,223,225,227,229,231,233,235,237,239,242,244,246],{"class":89,"line":138},[87,224,176],{"class":104},[87,226,180],{"class":179},[87,228,183],{"class":108},[87,230,186],{"class":104},[87,232,190],{"class":189},[87,234,193],{"class":104},[87,236,196],{"class":108},[87,238,199],{"class":104},[87,240,241],{"class":189},"'isAdmin'",[87,243,205],{"class":104},[87,245,190],{"class":189},[87,247,210],{"class":104},[87,249,250],{"class":89,"line":144},[87,251,252],{"class":215},"\u003C!-- type \"true\", form.values.isAdmin === true (a boolean) -->\n",[73,254,256],{"id":255},"disabling","Disabling",[78,258,260],{"className":80,"code":259,"language":82,"meta":83,"style":83},"useForm({ schema, coerce: false })\n",[18,261,262],{"__ignoreMap":83},[87,263,264,267,270,273],{"class":89,"line":90},[87,265,266],{"class":108},"useForm",[87,268,269],{"class":104},"({ schema, coerce: ",[87,271,272],{"class":97},"false",[87,274,275],{"class":104}," })\n",[14,277,278],{},"Without coercion the slim-primitive gate rejects type mismatches\nwith a dev-mode warning; the write doesn't land. Useful when you\nwant every typed-string write to fail loudly rather than silently\nbecome a number.",[73,280,282],{"id":281},"adding-a-custom-rule","Adding a custom rule",[14,284,285,288,289,292,293,295],{},[18,286,287],{},"defineCoercion"," narrows the ",[18,290,291],{},"transform"," parameter so you don't\nhave to cast inside the body. Spread ",[18,294,55],{}," to\nextend rather than replace:",[78,297,299],{"className":80,"code":298,"language":82,"meta":83,"style":83},"import { defineCoercion, defaultCoercionRules } from 'attaform'\nimport type { CoercionRegistry } from 'attaform'\n\nconst stringToBigint = defineCoercion({\n  input: 'string',\n  output: 'bigint',\n  transform: (s) => {\n    const trimmed = s.trim()\n    if (trimmed === '') return { coerced: false }\n    try {\n      return { coerced: true, value: BigInt(trimmed) }\n    } catch {\n      return { coerced: false } \u002F\u002F not a valid bigint literal\n    }\n  },\n})\n\nconst myRegistry: CoercionRegistry = [...defaultCoercionRules, stringToBigint]\n\nuseForm({ schema, coerce: myRegistry })\n",[18,300,301,315,329,333,347,358,368,390,410,438,446,465,476,491,497,503,508,513,538,543],{"__ignoreMap":83},[87,302,303,306,309,312],{"class":89,"line":90},[87,304,305],{"class":93},"import",[87,307,308],{"class":104}," { defineCoercion, defaultCoercionRules } ",[87,310,311],{"class":93},"from",[87,313,314],{"class":189}," 'attaform'\n",[87,316,317,319,322,325,327],{"class":89,"line":115},[87,318,305],{"class":93},[87,320,321],{"class":93}," type",[87,323,324],{"class":104}," { CoercionRegistry } ",[87,326,311],{"class":93},[87,328,314],{"class":189},[87,330,331],{"class":89,"line":127},[87,332,148],{"emptyLinePlaceholder":147},[87,334,335,337,340,342,345],{"class":89,"line":138},[87,336,94],{"class":93},[87,338,339],{"class":97}," stringToBigint",[87,341,101],{"class":93},[87,343,344],{"class":108}," defineCoercion",[87,346,112],{"class":104},[87,348,349,352,355],{"class":89,"line":144},[87,350,351],{"class":104},"  input: ",[87,353,354],{"class":189},"'string'",[87,356,357],{"class":104},",\n",[87,359,360,363,366],{"class":89,"line":151},[87,361,362],{"class":104},"  output: ",[87,364,365],{"class":189},"'bigint'",[87,367,357],{"class":104},[87,369,371,374,377,381,384,387],{"class":89,"line":370},7,[87,372,373],{"class":108},"  transform",[87,375,376],{"class":104},": (",[87,378,380],{"class":379},"s4XuR","s",[87,382,383],{"class":104},") ",[87,385,386],{"class":93},"=>",[87,388,389],{"class":104}," {\n",[87,391,393,396,399,401,404,407],{"class":89,"line":392},8,[87,394,395],{"class":93},"    const",[87,397,398],{"class":97}," trimmed",[87,400,101],{"class":93},[87,402,403],{"class":104}," s.",[87,405,406],{"class":108},"trim",[87,408,409],{"class":104},"()\n",[87,411,413,416,419,422,425,427,430,433,435],{"class":89,"line":412},9,[87,414,415],{"class":93},"    if",[87,417,418],{"class":104}," (trimmed ",[87,420,421],{"class":93},"===",[87,423,424],{"class":189}," ''",[87,426,383],{"class":104},[87,428,429],{"class":93},"return",[87,431,432],{"class":104}," { coerced: ",[87,434,272],{"class":97},[87,436,437],{"class":104}," }\n",[87,439,441,444],{"class":89,"line":440},10,[87,442,443],{"class":93},"    try",[87,445,389],{"class":104},[87,447,449,452,454,456,459,462],{"class":89,"line":448},11,[87,450,451],{"class":93},"      return",[87,453,432],{"class":104},[87,455,36],{"class":97},[87,457,458],{"class":104},", value: ",[87,460,461],{"class":108},"BigInt",[87,463,464],{"class":104},"(trimmed) }\n",[87,466,468,471,474],{"class":89,"line":467},12,[87,469,470],{"class":104},"    } ",[87,472,473],{"class":93},"catch",[87,475,389],{"class":104},[87,477,479,481,483,485,488],{"class":89,"line":478},13,[87,480,451],{"class":93},[87,482,432],{"class":104},[87,484,272],{"class":97},[87,486,487],{"class":104}," } ",[87,489,490],{"class":215},"\u002F\u002F not a valid bigint literal\n",[87,492,494],{"class":89,"line":493},14,[87,495,496],{"class":104},"    }\n",[87,498,500],{"class":89,"line":499},15,[87,501,502],{"class":104},"  },\n",[87,504,506],{"class":89,"line":505},16,[87,507,141],{"class":104},[87,509,511],{"class":89,"line":510},17,[87,512,148],{"emptyLinePlaceholder":147},[87,514,516,518,521,524,527,529,532,535],{"class":89,"line":515},18,[87,517,94],{"class":93},[87,519,520],{"class":97}," myRegistry",[87,522,523],{"class":93},":",[87,525,526],{"class":108}," CoercionRegistry",[87,528,101],{"class":93},[87,530,531],{"class":104}," [",[87,533,534],{"class":93},"...",[87,536,537],{"class":104},"defaultCoercionRules, stringToBigint]\n",[87,539,541],{"class":89,"line":540},19,[87,542,148],{"emptyLinePlaceholder":147},[87,544,546,548],{"class":89,"line":545},20,[87,547,266],{"class":108},[87,549,550],{"class":104},"({ schema, coerce: myRegistry })\n",[14,552,553,554,557],{},"Returning ",[18,555,556],{},"{ coerced: false }"," is the \"this rule doesn't apply\"\nsignal — the write passes through untouched. Use it for\nempty-input \u002F out-of-range \u002F parse-failure cases; leaving the\nwrite as-is lets the schema's refinement layer surface a\nValidationError instead of synthesising a wrong value.",[73,559,561],{"id":560},"replacing-the-defaults-entirely","Replacing the defaults entirely",[14,563,564],{},"Pass a registry without spreading the defaults:",[78,566,568],{"className":80,"code":567,"language":82,"meta":83,"style":83},"useForm({ schema, coerce: [stringToBigint] })\n\u002F\u002F string→number and string→boolean are NOT registered.\n",[18,569,570,577],{"__ignoreMap":83},[87,571,572,574],{"class":89,"line":90},[87,573,266],{"class":108},[87,575,576],{"class":104},"({ schema, coerce: [stringToBigint] })\n",[87,578,579],{"class":89,"line":115},[87,580,581],{"class":215},"\u002F\u002F string→number and string→boolean are NOT registered.\n",[14,583,584],{},"Attaform never merges past the array boundary — passing a registry is a\n\"replace\" operation by design.",[73,586,588],{"id":587},"app-level-default","App-level default",[14,590,591],{},"Set once via the plugin (matches the per-form shape):",[78,593,595],{"className":80,"code":594,"language":82,"meta":83,"style":83},"createAttaform({\n  defaults: { coerce: [...defaultCoercionRules, stringToBigint] },\n})\n",[18,596,597,604,614],{"__ignoreMap":83},[87,598,599,602],{"class":89,"line":90},[87,600,601],{"class":108},"createAttaform",[87,603,112],{"class":104},[87,605,606,609,611],{"class":89,"line":115},[87,607,608],{"class":104},"  defaults: { coerce: [",[87,610,534],{"class":93},[87,612,613],{"class":104},"defaultCoercionRules, stringToBigint] },\n",[87,615,616],{"class":89,"line":127},[87,617,141],{"class":104},[14,619,620,621,624],{},"Per-form ",[18,622,623],{},"useForm({ coerce })"," overrides the plugin default per\nform (replace, not merge — same semantics as everywhere else in\nAttaform).",[73,626,628],{"id":627},"pipeline-ordering","Pipeline ordering",[14,630,631],{},"For a single user-typed write, Attaform applies (in order):",[78,633,638],{"className":634,"code":636,"language":637},[635],"language-text","DOM event → modifier cast → coerce → transforms[0..n] → assigner\n","text",[18,639,636],{"__ignoreMap":83},[14,641,642,643,60,645,648,649,652,653,658],{},"Modifier casts (",[18,644,43],{},[18,646,647],{},".trim",") come from the directive itself;\nthey fire before coerce. ",[18,650,651],{},"transforms"," (see\n",[654,655,657],"a",{"href":656},".\u002Ftransforms","transforms recipe",") fire after coerce. The\nassigner is the last step before storage.",[73,660,662],{"id":661},"whats-not-coerced","What's NOT coerced",[664,665,666,677,685],"ul",{},[667,668,669,672,673,676],"li",{},[49,670,671],{},"Programmatic writes"," — ",[18,674,675],{},"form.setValue('age', '25')"," does NOT\ncoerce. The slim-primitive gate rejects the string write\n(programmatic writes are authoritative; the caller's typing is\non them).",[667,678,679,684],{},[49,680,681],{},[18,682,683],{},"form.reset()"," \u002F hydration \u002F SSR replay — already\nschema-conformant; running coerce would be redundant.",[667,686,687,690,691,693,694,697,698,701,702,705],{},[49,688,689],{},"Refinement-failure cases"," — coerce only retypes between slim\nprimitive kinds. A ",[18,692,59],{}," coerce produces a number;\na ",[18,695,696],{},"z.number().min(18)"," schema then validates, and a typed ",[18,699,700],{},"'5'","\nbecomes ",[18,703,704],{},"5"," and surfaces as a refinement error.",[73,707,709],{"id":708},"when-the-rules-output-disagrees-with-the-schema","When the rule's output disagrees with the schema",[14,711,712,713,716],{},"If the schema accepts BOTH the input and output kinds at a path\n(",[18,714,715],{},"z.union([z.string(), z.number()])","), coercion is silently\nskipped at that path — the schema's union is an explicit\n\"either is fine\" signal, and silent retyping would surprise.\nThe user-typed string lands as-is, and the schema picks the\nmatching branch.",[14,718,719,720,722,723,726],{},"If the rule's ",[18,721,291],{}," returns a value whose runtime kind\ndoesn't match the declared ",[18,724,725],{},"output",", the write passes through\nuntouched and a dev-mode warning surfaces. Post-validation\ndefends against buggy consumer rules without forcing every\nrule body to validate itself.",[728,729,730],"style",{},"html pre.shiki code .szBVR, html code.shiki .szBVR{--shiki-default:#D73A49;--shiki-dark:#F97583}html pre.shiki code .sj4cs, html code.shiki .sj4cs{--shiki-default:#005CC5;--shiki-dark:#79B8FF}html pre.shiki code .sVt8B, html code.shiki .sVt8B{--shiki-default:#24292E;--shiki-dark:#E1E4E8}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 .s9eBZ, html code.shiki .s9eBZ{--shiki-default:#22863A;--shiki-dark:#85E89D}html pre.shiki code .sZZnC, html code.shiki .sZZnC{--shiki-default:#032F62;--shiki-dark:#9ECBFF}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":83,"searchDepth":115,"depth":115,"links":732},[733,734,735,736,737,738,739,740],{"id":75,"depth":115,"text":76},{"id":255,"depth":115,"text":256},{"id":281,"depth":115,"text":282},{"id":560,"depth":115,"text":561},{"id":587,"depth":115,"text":588},{"id":627,"depth":115,"text":628},{"id":661,"depth":115,"text":662},{"id":708,"depth":115,"text":709},"Attaform coerces user-typed DOM values to the schema's slim type at the\ndirective layer — '25' → 25 for a z.number() slot, 'true' →\ntrue for a z.boolean() slot. The schema is authoritative for\nstorage shape; consumers stop sprinkling .number modifiers across\ntemplates.","md",{},"\u002Fdocs\u002Frecipes\u002Fcoerce",{"title":5,"description":741},"docs\u002Frecipes\u002Fcoerce","RB2bdQeqfyFgYb3FXroemoc6ISxOs3z50UeQrmjPG8k",1777934136208]