[{"data":1,"prerenderedAt":524},["ShallowReactive",2],{"content-\u002Fdocs\u002Fvalidation\u002Fper-field-validation":3},{"id":4,"title":5,"body":6,"description":502,"extension":503,"meta":504,"metaRows":505,"navigation":518,"path":519,"seo":520,"source":521,"stem":522,"__hash__":523},"docs\u002Fdocs\u002Fvalidation\u002Fper-field-validation.md","Per-field validation",{"type":7,"value":8,"toc":493},"minimark",[9,13,20,23,41,45,49,56,61,64,191,214,218,229,327,340,344,350,404,418,422,438,442,489],[10,11,5],"h1",{"id":12},"per-field-validation",[14,15,16],"blockquote",{},[17,18,19],"p",{},"The schema is the validator. Chain refinements onto one leaf, or attach cross-field rules to the parent with a target path.",[21,22],"docs-meta-table",{},[17,24,25,26,30,31,34,35,40],{},"Type into each field to watch its own refinements light up: the username's regex requirement, the password's min length, and the cross-field \"passwords must match\" check that fires when ",[27,28,29],"code",{},"confirmPassword"," differs from ",[27,32,33],{},"password",". Both single-field and cross-field validators live in the schema; the ",[36,37,39],"a",{"href":38},"#two-patterns","Two patterns"," section traces each.",[42,43],"docs-demo",{"label":44,"slug":12},"Per-field Validation Demo",[46,47,39],"h2",{"id":48},"two-patterns",[17,50,51,52,55],{},"Attaform reads two Zod patterns for per-field validation, both surfacing errors at the right ",[27,53,54],{},"form.errors.\u003Cpath>",".",[57,58,60],"h3",{"id":59},"single-field-chains","Single-field chains",[17,62,63],{},"Every Zod primitive accepts a chain of refinements:",[65,66,71],"pre",{"className":67,"code":68,"language":69,"meta":70,"style":70},"language-ts shiki shiki-themes github-light github-dark","z.object({\n  username: z\n    .string()\n    .min(3, 'At least 3 characters')\n    .max(20, 'At most 20 characters')\n    .regex(\u002F^[a-z0-9_]+$\u002F, 'Lowercase letters, numbers, and underscores only'),\n})\n","ts","",[27,72,73,89,95,107,132,152,185],{"__ignoreMap":70},[74,75,78,82,86],"span",{"class":76,"line":77},"line",1,[74,79,81],{"class":80},"sVt8B","z.",[74,83,85],{"class":84},"sScJk","object",[74,87,88],{"class":80},"({\n",[74,90,92],{"class":76,"line":91},2,[74,93,94],{"class":80},"  username: z\n",[74,96,98,101,104],{"class":76,"line":97},3,[74,99,100],{"class":80},"    .",[74,102,103],{"class":84},"string",[74,105,106],{"class":80},"()\n",[74,108,110,112,115,118,122,125,129],{"class":76,"line":109},4,[74,111,100],{"class":80},[74,113,114],{"class":84},"min",[74,116,117],{"class":80},"(",[74,119,121],{"class":120},"sj4cs","3",[74,123,124],{"class":80},", ",[74,126,128],{"class":127},"sZZnC","'At least 3 characters'",[74,130,131],{"class":80},")\n",[74,133,135,137,140,142,145,147,150],{"class":76,"line":134},5,[74,136,100],{"class":80},[74,138,139],{"class":84},"max",[74,141,117],{"class":80},[74,143,144],{"class":120},"20",[74,146,124],{"class":80},[74,148,149],{"class":127},"'At most 20 characters'",[74,151,131],{"class":80},[74,153,155,157,160,162,165,169,172,175,177,179,182],{"class":76,"line":154},6,[74,156,100],{"class":80},[74,158,159],{"class":84},"regex",[74,161,117],{"class":80},[74,163,164],{"class":127},"\u002F",[74,166,168],{"class":167},"szBVR","^",[74,170,171],{"class":120},"[a-z0-9_]",[74,173,174],{"class":167},"+$",[74,176,164],{"class":127},[74,178,124],{"class":80},[74,180,181],{"class":127},"'Lowercase letters, numbers, and underscores only'",[74,183,184],{"class":80},"),\n",[74,186,188],{"class":76,"line":187},7,[74,189,190],{"class":80},"})\n",[17,192,193,194,197,198,201,202,205,206,209,210,213],{},"Each refinement's error message appears at ",[27,195,196],{},"errors.username",". The order matters: refinements stop at the first failure, so ",[27,199,200],{},".min(3)"," runs before ",[27,203,204],{},".regex",". Read the field's ",[27,207,208],{},"firstError"," to get the first failure's message; the full array is available at ",[27,211,212],{},"errors.\u003Cpath>"," for surfacing every refinement that fired.",[57,215,217],{"id":216},"cross-field-refinements","Cross-field refinements",[17,219,220,221,224,225,228],{},"For checks that involve multiple paths, attach ",[27,222,223],{},".refine"," to the parent object and use the ",[27,226,227],{},"path"," option to target where the error lands:",[65,230,232],{"className":67,"code":231,"language":69,"meta":70,"style":70},"z.object({\n  password: z.string().min(8),\n  confirmPassword: z.string(),\n}).refine((data) => data.password === data.confirmPassword, {\n  message: 'Passwords must match',\n  path: ['confirmPassword'],\n})\n",[27,233,234,242,261,271,301,312,323],{"__ignoreMap":70},[74,235,236,238,240],{"class":76,"line":77},[74,237,81],{"class":80},[74,239,85],{"class":84},[74,241,88],{"class":80},[74,243,244,247,249,252,254,256,259],{"class":76,"line":91},[74,245,246],{"class":80},"  password: z.",[74,248,103],{"class":84},[74,250,251],{"class":80},"().",[74,253,114],{"class":84},[74,255,117],{"class":80},[74,257,258],{"class":120},"8",[74,260,184],{"class":80},[74,262,263,266,268],{"class":76,"line":97},[74,264,265],{"class":80},"  confirmPassword: z.",[74,267,103],{"class":84},[74,269,270],{"class":80},"(),\n",[74,272,273,276,279,282,286,289,292,295,298],{"class":76,"line":109},[74,274,275],{"class":80},"}).",[74,277,278],{"class":84},"refine",[74,280,281],{"class":80},"((",[74,283,285],{"class":284},"s4XuR","data",[74,287,288],{"class":80},") ",[74,290,291],{"class":167},"=>",[74,293,294],{"class":80}," data.password ",[74,296,297],{"class":167},"===",[74,299,300],{"class":80}," data.confirmPassword, {\n",[74,302,303,306,309],{"class":76,"line":134},[74,304,305],{"class":80},"  message: ",[74,307,308],{"class":127},"'Passwords must match'",[74,310,311],{"class":80},",\n",[74,313,314,317,320],{"class":76,"line":154},[74,315,316],{"class":80},"  path: [",[74,318,319],{"class":127},"'confirmPassword'",[74,321,322],{"class":80},"],\n",[74,324,325],{"class":76,"line":187},[74,326,190],{"class":80},[17,328,329,330,332,333,336,337,339],{},"The refinement runs against the whole parent value; the ",[27,331,227],{}," option directs the error to ",[27,334,335],{},"errors.confirmPassword"," so the UI surfaces it next to the right input. Without ",[27,338,227],{},", the error lands at the parent path (the form root in this example).",[46,341,343],{"id":342},"custom-per-field-refinements","Custom per-field refinements",[17,345,346,347,349],{},"For predicates beyond the built-in chain, use ",[27,348,223],{}," at the leaf:",[65,351,353],{"className":67,"code":352,"language":69,"meta":70,"style":70},"z.string().refine((v) => !v.includes('admin'), {\n  message: 'Reserved word, pick another username',\n})\n",[27,354,355,391,400],{"__ignoreMap":70},[74,356,357,359,361,363,365,367,370,372,374,377,380,383,385,388],{"class":76,"line":77},[74,358,81],{"class":80},[74,360,103],{"class":84},[74,362,251],{"class":80},[74,364,278],{"class":84},[74,366,281],{"class":80},[74,368,369],{"class":284},"v",[74,371,288],{"class":80},[74,373,291],{"class":167},[74,375,376],{"class":167}," !",[74,378,379],{"class":80},"v.",[74,381,382],{"class":84},"includes",[74,384,117],{"class":80},[74,386,387],{"class":127},"'admin'",[74,389,390],{"class":80},"), {\n",[74,392,393,395,398],{"class":76,"line":91},[74,394,305],{"class":80},[74,396,397],{"class":127},"'Reserved word, pick another username'",[74,399,311],{"class":80},[74,401,402],{"class":76,"line":97},[74,403,190],{"class":80},[17,405,406,407,410,411,414,415,55],{},"The function receives the parsed leaf value; return ",[27,408,409],{},"true"," to accept, ",[27,412,413],{},"false"," to reject. The message string surfaces as the error's ",[27,416,417],{},".message",[46,419,421],{"id":420},"sync-vs-async","Sync vs async",[17,423,424,425,429,430,434,435,437],{},"Sync refinements run on every validation pass: keystroke, blur, submit (per the ",[36,426,428],{"href":427},"\u002Fdocs\u002Fvalidation\u002Fwhen-validation-runs","validateOn config","). For checks that need a server round-trip (uniqueness probes, slug availability, password-breach lookups), reach for ",[36,431,433],{"href":432},"\u002Fdocs\u002Fvalidation\u002Fasync-refinements","async refinements",". Zod's ",[27,436,223],{}," accepts an async predicate, and Attaform awaits it before submit dispatches.",[46,439,441],{"id":440},"where-to-next","Where to next",[443,444,445,452,469,479],"ul",{},[446,447,448,451],"li",{},[36,449,450],{"href":432},"Async refinements",": predicates that await a server round-trip.",[446,453,454,458,459,124,462,124,465,468],{},[36,455,457],{"href":456},"\u002Fdocs\u002Fvalidation\u002Flifecycle","The validation lifecycle",": the three imperative methods (",[27,460,461],{},"validate",[27,463,464],{},"validateAsync",[27,466,467],{},"process",").",[446,470,471,474,475,478],{},[36,472,473],{"href":427},"When validation runs",": the ",[27,476,477],{},"validateOn"," timing knob.",[446,480,481,474,485,488],{},[36,482,484],{"href":483},"\u002Fdocs\u002Fvalidation\u002Fshowing-errors","Display state and showing errors",[27,486,487],{},"getDisplayState"," predicate.",[490,491,492],"style",{},"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 pre.shiki code .sj4cs, html code.shiki .sj4cs{--shiki-default:#005CC5;--shiki-dark:#79B8FF}html pre.shiki code .sZZnC, html code.shiki .sZZnC{--shiki-default:#032F62;--shiki-dark:#9ECBFF}html pre.shiki code .szBVR, html code.shiki .szBVR{--shiki-default:#D73A49;--shiki-dark:#F97583}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 .s4XuR, html code.shiki .s4XuR{--shiki-default:#E36209;--shiki-dark:#FFAB70}",{"title":70,"searchDepth":91,"depth":91,"links":494},[495,499,500,501],{"id":48,"depth":91,"text":39,"children":496},[497,498],{"id":59,"depth":97,"text":60},{"id":216,"depth":97,"text":217},{"id":342,"depth":91,"text":343},{"id":420,"depth":91,"text":421},{"id":440,"depth":91,"text":441},"Schema-level chains attach validators to one field at a time; .refine on the parent object attaches cross-field refinements with a target path. Both surface the error at the right field.","md",{},[506,509,512,515],{"label":507,"value":508},"Category","Schema pattern",{"label":510,"value":511,"kind":27},"Single-field",".min · .max · .regex · .email · .refine (per-leaf)",{"label":513,"value":514,"kind":27},"Cross-field","parent.refine(checker, { path })",{"label":516,"value":517,"kind":27},"Errors surface at","errors.\u003CtargetPath>",true,"\u002Fdocs\u002Fvalidation\u002Fper-field-validation",{"title":5,"description":502},null,"docs\u002Fvalidation\u002Fper-field-validation","t6982omiJC4cAi3fbtNrcxAYhhBplj1FQmc8Lk_wk74",1780949760153]