{"record":{"id":"a66ddefc7cf9d24b","repo":"affaan-m/ECC","slug":"passwords-do-not-match","errorCode":null,"errorMessage":"Passwords do not match","messagePattern":"Passwords do not match","errorType":"validation","errorClass":"ValueError","httpStatus":null,"severity":"warning","filePath":"skills/fastapi-patterns/SKILL.md","lineNumber":138,"sourceCode":"```python\n# app/schemas/user.py\nfrom datetime import datetime\nfrom pydantic import BaseModel, EmailStr, Field, model_validator\n\n\nclass UserBase(BaseModel):\n    email: EmailStr\n    username: str = Field(min_length=3, max_length=50)\n\n\nclass UserCreate(UserBase):\n    password: str = Field(min_length=8)\n    password_confirm: str\n\n    @model_validator(mode=\"after\")\n    def passwords_match(self) -> \"UserCreate\":\n        if self.password != self.password_confirm:\n            raise ValueError(\"Passwords do not match\")\n        return self\n\n\nclass UserUpdate(BaseModel):\n    username: str | None = Field(default=None, min_length=3, max_length=50)\n    email: EmailStr | None = None\n\n\nclass UserResponse(UserBase):\n    id: int\n    is_active: bool\n    created_at: datetime\n\n    model_config = {\"from_attributes\": True}\n\n\nclass UserListResponse(BaseModel):\n    total: int","sourceCodeStart":120,"sourceCodeEnd":156,"githubUrl":"https://github.com/affaan-m/ECC/blob/01e15490f04e29cfefe3896951f43db46994d8ee/skills/fastapi-patterns/SKILL.md#L120-L156","documentation":"A Pydantic v2 `model_validator(mode=\"after\")` on the `UserCreate` schema raises `ValueError` when `password != password_confirm`. Pydantic converts a validator `ValueError` into a `ValidationError` that FastAPI surfaces as HTTP 422 with a field-level error. `password_confirm` is a transport-only field and is not persisted.","triggerScenarios":"POST `/users/` (or `/`) with a JSON body where `password` and `password_confirm` differ, or where `password_confirm` is missing (Pydantic raises a missing-field error first). Front-end sends a stale form after the user edited one password box but not the second.","commonSituations":"JS client only sends `password` and forgets `password_confirm` (422 on missing field, not the mismatch). Two password inputs get out of sync due to autofill or paste. Tests construct `UserCreate(...)` directly with mismatched values.","solutions":["Check the request body: both fields present and equal before submit (client-side `password === passwordConfirm`).","If the 422 lists the error under `password_confirm`, the values differ — re-enter and resubmit.","Confirm the schema field name matches what the client sends (`password_confirm`, not `passwordConfirm` or `confirm_password`).","If you do not want a 422, drop the validator and rely on a server-side hash+compare; but the mismatch check is the recommended pattern."],"exampleFix":"# before\n@model_validator(mode=\"after\")\ndef passwords_match(self) -> \"UserCreate\":\n    if self.password != self.password_confirm:\n        raise ValueError(\"Passwords do not match\")\n    return self\n\n# after — clear, attributed error so FastAPI's 422 points at the right field\nfrom pydantic import model_validator\n@model_validator(mode=\"after\")\ndef passwords_match(self) -> \"UserCreate\":\n    if self.password != self.password_confirm:\n        raise ValueError({\"password_confirm\": \"Passwords do not match\"})\n    return self","handlingStrategy":"validation","validationCode":"# client-side, before submit\nif form.password !== form.password_confirm:\n    setFieldError('password_confirm', 'Passwords do not match')\n    return\nawait fetch('/users/', {method:'POST', body: JSON.stringify(form)})","typeGuard":"from pydantic import BaseModel\ndef has_matching_passwords(data: dict) -> bool:\n    return isinstance(data, dict) and data.get('password') == data.get('password_confirm') and bool(data.get('password'))","tryCatchPattern":"from pydantic import ValidationError\ntry:\n    user_in = UserCreate(**payload)\nexcept ValidationError as e:\n    # 422 body; surface password_confirm error to that input\n    return json_response(e.errors(), status=422)","preventionTips":["Disable submit until both password fields are non-empty and equal.","Keep the field name `password_confirm` consistent across client and server.","Drop `password_confirm` before persistence (model_dump(exclude={'password_confirm'}))."],"tags":["fastapi","pydantic","validation","auth","schemas"],"backgroundTag":null,"analyzedSha":"01e15490f04e29cfefe3896951f43db46994d8ee","analyzedAt":"2026-08-13T00:31:08.655Z","schemaVersion":2},"datasetVersion":"2026-08-13T04:17:16.726Z"}