[{"data":1,"prerenderedAt":96},["ShallowReactive",2],{"qa-\u002Ffastapi\u002Fpydantic\u002Fvalidation":3},{"page":4,"siblings":82,"blog":73},{"id":5,"title":6,"body":7,"description":11,"difficulty":14,"extension":15,"framework":16,"frameworkSlug":17,"meta":18,"navigation":19,"order":12,"path":20,"questions":21,"questionsCount":72,"related":73,"seo":74,"seoDescription":75,"stem":76,"subtopic":77,"topic":78,"topicSlug":79,"updated":80,"__hash__":81},"qa\u002Ffastapi\u002Fpydantic\u002Fvalidation.md","Validation",{"type":8,"value":9,"toc":10},"minimark",[],{"title":11,"searchDepth":12,"depth":12,"links":13},"",2,[],"medium","md","FastAPI","fastapi",{},true,"\u002Ffastapi\u002Fpydantic\u002Fvalidation",[22,26,30,34,38,43,47,51,55,59,64,68],{"id":23,"difficulty":14,"q":24,"a":25},"field-validator-basics","How do you write a custom field validator in Pydantic v2?","Use `@field_validator(\"field_name\")` decorator on a classmethod:\n\n```python\nfrom pydantic import BaseModel, field_validator\n\nclass User(BaseModel):\n    username: str\n    age: int\n\n    @field_validator(\"username\")\n    @classmethod\n    def username_must_be_alphanumeric(cls, v: str) -> str:\n        if not v.isalnum():\n            raise ValueError(\"username must be alphanumeric\")\n        return v.lower()   # transform the value\n\n    @field_validator(\"age\")\n    @classmethod\n    def age_must_be_positive(cls, v: int) -> int:\n        if v \u003C 0:\n            raise ValueError(\"age must be non-negative\")\n        return v\n```\n\n`ValueError` messages are caught by Pydantic and included in the `ValidationError`\ndetail. You can also raise `PydanticCustomError` for more structured errors.\n\nRule of thumb: field validators run per-field; use them for constraints that\ncan't be expressed with `Field()` parameters (cross-value logic belongs in\nmodel validators).\n",{"id":27,"difficulty":14,"q":28,"a":29},"field-validator-modes","What are the `before`, `after`, `wrap` and `plain` modes for `@field_validator`?","`mode` controls when the validator runs relative to Pydantic's built-in type coercion:\n\n| Mode | Runs | Input type | Use case |\n|------|------|------------|----------|\n| `\"before\"` (default v2) | before coercion | raw input (str\u002Fdict\u002Fetc.) | normalize raw strings |\n| `\"after\"` | after coercion | declared Python type | validate typed value |\n| `\"wrap\"` | wraps coercion | raw + handler callable | conditional coercion |\n| `\"plain\"` | replaces coercion | raw input | fully custom type parsing |\n\n```python\nfrom pydantic import BaseModel, field_validator\n\nclass Order(BaseModel):\n    amount: float\n\n    @field_validator(\"amount\", mode=\"before\")\n    @classmethod\n    def strip_currency_symbol(cls, v):\n        if isinstance(v, str):\n            return v.lstrip(\"$£€\")   # \"£9.99\" → \"9.99\"\n        return v\n```\n\nRule of thumb: use `mode=\"before\"` to normalise raw strings before Pydantic\ntries to parse them; use `mode=\"after\"` for logic that needs the typed value.\n",{"id":31,"difficulty":14,"q":32,"a":33},"multiple-fields-validator","How do you validate multiple fields with a single `@field_validator`?","Pass multiple field names to `@field_validator`. The validator is called once\nper field listed:\n\n```python\nfrom pydantic import BaseModel, field_validator\n\nclass Product(BaseModel):\n    name: str\n    description: str\n\n    @field_validator(\"name\", \"description\")\n    @classmethod\n    def must_not_be_blank(cls, v: str) -> str:\n        if not v.strip():\n            raise ValueError(\"must not be blank or whitespace\")\n        return v.strip()\n```\n\nThe validator receives one field value at a time — it does not receive both\nsimultaneously. For cross-field logic, use `@model_validator`.\n\nRule of thumb: share validators across fields with multiple names in the\ndecorator to keep DRY; use `@model_validator` when the logic depends on comparing\nfield values to each other.\n",{"id":35,"difficulty":14,"q":36,"a":37},"model-validator","What is `@model_validator` and when do you need it instead of `@field_validator`?","`@model_validator` runs once on the **whole model** (all fields at once). Use it\nfor cross-field constraints.\n\n```python\nfrom pydantic import BaseModel, model_validator\n\nclass DateRange(BaseModel):\n    start: date\n    end: date\n\n    @model_validator(mode=\"after\")\n    def end_after_start(self) -> \"DateRange\":\n        if self.end \u003C= self.start:\n            raise ValueError(\"end must be after start\")\n        return self\n```\n\n`mode=\"after\"` — runs after all fields are coerced and validated; `self` is the\nmodel instance.\n`mode=\"before\"` — runs before field validation; receives raw data as a dict.\n\nRule of thumb: `@field_validator` for single-field logic; `@model_validator(mode=\"after\")`\nfor cross-field constraints like \"end after start\" or \"confirm password matches\".\n",{"id":39,"difficulty":40,"q":41,"a":42},"model-validator-before","hard","When would you use `@model_validator(mode=\"before\")` and what does it receive?","`mode=\"before\"` intercepts the raw input **before any field parsing**. The\nvalidator receives a dict (or arbitrary input) and must return a dict.\n\n```python\nfrom pydantic import BaseModel, model_validator\n\nclass FlexibleUser(BaseModel):\n    name: str\n    email: str\n\n    @model_validator(mode=\"before\")\n    @classmethod\n    def coerce_legacy_format(cls, data):\n        # support both {\"name\": ...} and {\"full_name\": ...}\n        if \"full_name\" in data and \"name\" not in data:\n            data[\"name\"] = data.pop(\"full_name\")\n        return data\n```\n\nUse cases: renaming legacy fields, setting computed defaults before field\nvalidation, accepting multiple input shapes.\n\nRule of thumb: `mode=\"before\"` is for input normalisation at the whole-model\nlevel; prefer `mode=\"after\"` for validation because you get typed values.\n",{"id":44,"difficulty":40,"q":45,"a":46},"custom-type","How do you create a custom Pydantic type with its own validation logic?","Annotate a class with `__get_validators__` (v1) or implement `__get_pydantic_core_schema__`\n(v2). For simple cases, use `Annotated` with `AfterValidator`:\n\n```python\nfrom typing import Annotated\nfrom pydantic import AfterValidator, BaseModel\n\ndef validate_isbn(v: str) -> str:\n    v = v.replace(\"-\", \"\")\n    if len(v) not in (10, 13):\n        raise ValueError(\"ISBN must be 10 or 13 digits\")\n    return v\n\nISBN = Annotated[str, AfterValidator(validate_isbn)]\n\nclass Book(BaseModel):\n    title: str\n    isbn: ISBN\n```\n\nFor full custom types with JSON Schema:\n```python\nfrom pydantic import GetCoreSchemaHandler\nfrom pydantic_core import core_schema\n\nclass PositiveDecimal:\n    @classmethod\n    def __get_pydantic_core_schema__(cls, source, handler: GetCoreSchemaHandler):\n        return core_schema.no_info_after_validator_function(cls, core_schema.decimal_schema())\n```\n\nRule of thumb: use `Annotated[T, AfterValidator(fn)]` for simple reusable\nvalidators; implement `__get_pydantic_core_schema__` only for complex custom types.\n",{"id":48,"difficulty":14,"q":49,"a":50},"pydantic-error-structure","What does a `ValidationError` from Pydantic look like and how do you access its details?","`ValidationError` has an `.errors()` method returning a list of error dicts:\n\n```python\nfrom pydantic import BaseModel, ValidationError\n\nclass Item(BaseModel):\n    name: str\n    price: float\n\ntry:\n    Item(name=\"\", price=-1)\nexcept ValidationError as e:\n    print(e.errors())\n# [\n#   {\"type\": \"value_error\", \"loc\": (\"name\",), \"msg\": \"...\", \"input\": \"\"},\n#   {\"type\": \"greater_than\", \"loc\": (\"price\",), \"msg\": \"...\", \"input\": -1}\n# ]\n```\n\nEach error has:\n- `type` — Pydantic error code\n- `loc` — tuple of keys pinpointing the failing field\n- `msg` — human-readable message\n- `input` — the value that failed\n\nFastAPI captures this and returns it as the 422 response `detail` array.\n\nRule of thumb: in unit tests, assert on `exc.errors()[0][\"type\"]` to test\nspecific error codes rather than message strings that may change.\n",{"id":52,"difficulty":40,"q":53,"a":54},"pydantic-strict-mode","What is strict mode in Pydantic v2 and when would you enable it?","Strict mode disables Pydantic's **type coercion** — values must already be the\ncorrect Python type; no implicit conversion happens.\n\n```python\nfrom pydantic import BaseModel, ConfigDict\n\nclass StrictItem(BaseModel):\n    model_config = ConfigDict(strict=True)\n    price: float\n    count: int\n\nStrictItem(price=9.99, count=3)     # OK\nStrictItem(price=\"9.99\", count=3)   # ValidationError — \"9.99\" is str, not float\n```\n\nPer-field: `Field(strict=True)` or `Strict` annotated type.\n\nUse cases: data coming from other Python code (not raw JSON) where silent coercion\nhides bugs; internal domain objects that must have exact types.\n\nRule of thumb: don't use strict mode for API request models — clients send JSON\nwhere numbers may arrive as strings; use it for internal\u002FDTO models between Python layers.\n",{"id":56,"difficulty":14,"q":57,"a":58},"validator-skip-on-none","How do you skip a validator when the field value is `None`?","Pass `skip_on_failure=True` (for `mode=\"after\"`) or check for `None` at the top\nof the validator. With `mode=\"before\"` and optional fields, validators run even\non `None` inputs by default.\n\n```python\nfrom pydantic import BaseModel, field_validator\n\nclass Profile(BaseModel):\n    bio: str | None = None\n\n    @field_validator(\"bio\", mode=\"after\")\n    @classmethod\n    def bio_max_sentences(cls, v: str | None) -> str | None:\n        if v is None:\n            return v    # skip check\n        if v.count(\".\") > 5:\n            raise ValueError(\"bio may not exceed 5 sentences\")\n        return v\n```\n\nAlternatively, in Pydantic v2 you can annotate with `Optional` inside\n`Annotated` and chain with `BeforeValidator` that returns `None` early.\n\nRule of thumb: always guard against `None` at the top of validators for\noptional fields — a missed check causes a confusing `AttributeError`.\n",{"id":60,"difficulty":61,"q":62,"a":63},"validator-return-value","easy","Must a Pydantic field validator return a value?","Yes — the return value of a `@field_validator` replaces the field's value.\nForgetting to `return v` silently sets the field to `None`.\n\n```python\n# WRONG — returns None, field becomes None\n@field_validator(\"name\")\n@classmethod\ndef check_name(cls, v: str) -> str:\n    if not v:\n        raise ValueError(\"required\")\n    # forgot return v!\n\n# CORRECT\n@field_validator(\"name\")\n@classmethod\ndef check_name(cls, v: str) -> str:\n    if not v:\n        raise ValueError(\"required\")\n    return v   # must return the (possibly transformed) value\n```\n\nRule of thumb: end every `@field_validator` with a `return v` — validators\nthat only validate (no transform) still must return the original value.\n",{"id":65,"difficulty":14,"q":66,"a":67},"before-validator-annotated","What is `BeforeValidator` \u002F `AfterValidator` in Pydantic v2 `Annotated` style?","These are functional wrappers used inside `Annotated` to attach validators\nwithout subclassing `BaseModel`:\n\n```python\nfrom typing import Annotated\nfrom pydantic import BaseModel, BeforeValidator, AfterValidator\n\ndef strip_spaces(v: str) -> str:\n    return v.strip()\n\ndef ensure_lower(v: str) -> str:\n    return v.lower()\n\nCleanStr = Annotated[str, BeforeValidator(strip_spaces), AfterValidator(ensure_lower)]\n\nclass User(BaseModel):\n    username: CleanStr\n```\n\nThis creates **reusable annotated types** you can import and use across models\nwithout copy-pasting validators.\n\nRule of thumb: define common transformations as `Annotated` types (`Email`,\n`SlugStr`, `PositiveDecimal`) and import them — it's DRY and keeps validators\nout of model classes.\n",{"id":69,"difficulty":40,"q":70,"a":71},"cross-model-validation","How do you validate that two related request models are consistent with each other in FastAPI?","Accept both models as body parameters and validate their relationship in the\nhandler (or in a dedicated service function):\n\n```python\nclass DateRangeFilter(BaseModel):\n    start: date\n    end: date\n\n    @model_validator(mode=\"after\")\n    def end_after_start(self) -> \"DateRangeFilter\":\n        if self.end \u003C self.start:\n            raise ValueError(\"end must be >= start\")\n        return self\n\nclass ReportRequest(BaseModel):\n    range: DateRangeFilter\n    metrics: list[str]\n\n@app.post(\"\u002Freports\")\nasync def generate_report(req: ReportRequest):\n    ...\n```\n\nFor cross-model checks that span the body and path\u002Fquery params, perform the\ncheck in the handler and raise `HTTPException(422, ...)`.\n\nRule of thumb: encode single-model invariants in `@model_validator`; encode\ncross-model or cross-layer invariants in the handler or service layer.\n",12,null,{"description":11},"FastAPI Pydantic validator interview questions — field_validator, model_validator, before\u002Fafter mode, custom types and raising ValidationError.","fastapi\u002Fpydantic\u002Fvalidation","Validators","Pydantic & Validation","pydantic","2026-06-20","EXg87mVabg1_159V7NR7LEHuU_pfzrq0kolsiqv3NcI",[83,87,88,92],{"subtopic":84,"path":85,"order":86},"Pydantic Models","\u002Ffastapi\u002Fpydantic\u002Fmodels",1,{"subtopic":77,"path":20,"order":12},{"subtopic":89,"path":90,"order":91},"Serialization","\u002Ffastapi\u002Fpydantic\u002Fserialization",3,{"subtopic":93,"path":94,"order":95},"Settings Management","\u002Ffastapi\u002Fpydantic\u002Fsettings",4,1782244112825]