[{"data":1,"prerenderedAt":2441},["ShallowReactive",2],{"page-\u002Fgetting-started-with-python-apis-for-builders\u002Fparsing-json-responses\u002Fvalidating-json-with-pydantic-v2\u002F":3,"faq-schema-\u002Fgetting-started-with-python-apis-for-builders\u002Fparsing-json-responses\u002Fvalidating-json-with-pydantic-v2\u002F":2420},{"id":4,"title":5,"body":6,"description":2409,"extension":2410,"meta":2411,"navigation":429,"path":2416,"seo":2417,"stem":2418,"__hash__":2419},"content\u002Fgetting-started-with-python-apis-for-builders\u002Fparsing-json-responses\u002Fvalidating-json-with-pydantic-v2\u002Findex.md","Validating JSON with Pydantic v2: Turning Messy Payloads into Typed Data",{"type":7,"value":8,"toc":2396},"minimark",[9,13,23,26,31,39,52,199,201,205,227,245,381,670,690,692,696,707,1250,1271,1406,1408,1412,1441,1470,1608,1610,1614,1642,1838,1860,1933,1938,2009,2011,2015,2029,2098,2106,2109,2205,2227,2229,2233,2260,2264,2289,2295,2310,2330,2336,2340,2345,2371,2376,2392],[10,11,5],"h1",{"id":12},"validating-json-with-pydantic-v2-turning-messy-payloads-into-typed-data",[14,15,16,17,22],"p",{},"Every JSON response from an upstream API is untrusted input until something checks it. Fields go missing, types drift, an integer arrives as a string, and a contract you read in the docs last month quietly changed. Pydantic v2 turns that uncertainty into a typed Python object — or into a clear error at the boundary instead of a confusing crash three functions deep. This page covers the decisions that matter once you are charging money for the endpoint: which entry point to call, what Pydantic will silently coerce, how to configure a model differently for inbound requests than for upstream responses, and what validation actually costs per call. Part of the ",[18,19,21],"a",{"href":20},"\u002Fgetting-started-with-python-apis-for-builders\u002Fparsing-json-responses\u002F","Parsing JSON Responses"," guide.",[24,25],"hr",{},[27,28,30],"h2",{"id":29},"why-validate-json-at-the-boundary","Why Validate JSON at the Boundary",[14,32,33,34,38],{},"Both directions of JSON deserve a gate, and they fail differently. Inbound payloads to your own API can be malformed, malicious, or just wrong; validating them early gives the caller a clean 422 with a machine-readable body instead of a stack trace and a support ticket. Upstream responses from third-party APIs are the sneakier risk. You do not control them, they change without a changelog entry, and a missing field surfaces as an ",[35,36,37],"code",{},"AttributeError"," in a worker at 03:00 — far from the code that actually broke.",[14,40,41,42,46,47,51],{},"Validating at the boundary collapses both into one place. Once a payload has passed through a Pydantic model, the rest of your service works with a typed object: your editor autocompletes fields, the types are guaranteed, and failures are pinned to the exact moment data entered the system. That pairs directly with the rest of your boundary handling — ",[18,43,45],{"href":44},"\u002Fgetting-started-with-python-apis-for-builders\u002Fparsing-json-responses\u002Fdebugging-401-unauthorized-api-errors\u002F","debugging 401 unauthorized API errors"," covers the auth half of the same gate, and ",[18,48,50],{"href":49},"\u002Fgetting-started-with-python-apis-for-builders\u002Fparsing-json-responses\u002Fhandling-large-json-payloads-with-streaming\u002F","handling large JSON payloads with streaming"," covers the case where the payload is too big to validate in one shot.",[53,54,62,63,62,67,62,71,62,90,62,97,62,105,62,114,62,120,62,125,62,128,62,132,62,136,62,144,62,148,62,156,62,162,62,167,62,171,62,173,62,178,62,182,62,185,62,188,62,191,62,194],"svg",{"viewBox":55,"role":56,"ariaLabelledBy":57,"xmlns":60,"style":61},"0 0 720 260","img",[58,59],"pyd2-gate-t","pyd2-gate-d","http:\u002F\u002Fwww.w3.org\u002F2000\u002Fsvg","width:100%;height:auto;margin:1.5rem 0;font-family:var(--font-sans);","\n  ",[64,65,66],"title",{"id":58},"One validation gate serving both JSON directions",[68,69,70],"desc",{"id":59},"Inbound request bodies and upstream API responses both pass through a single Pydantic model, which emits either a typed object for business logic or a structured ValidationError mapped to a 422 response.",[72,73,74,75,62],"defs",{},"\n    ",[76,77,84,85,74],"marker",{"id":78,"viewBox":79,"refX":80,"refY":81,"markerWidth":82,"markerHeight":82,"orient":83},"pyd2-gate-arrow","0 0 10 10","9","5","7","auto-start-reverse","\n      ",[86,87],"path",{"d":88,"fill":89},"M0 0 L10 5 L0 10 z","var(--c-text-muted)",[91,92],"rect",{"x":93,"y":93,"width":94,"height":95,"fill":96},"0","720","260","var(--c-surface)",[98,99,104],"text",{"x":100,"y":101,"fill":102,"style":103},"16","26","var(--c-text)","font-size:13;font-family:var(--font-sans);","One gate, two directions of untrusted JSON",[91,106],{"x":107,"y":108,"width":109,"height":110,"rx":111,"fill":96,"stroke":112,"style":113},"20","60","190","54","10","var(--c-coral)","stroke-width:2;",[98,115,119],{"x":116,"y":117,"fill":102,"style":118},"115","82","text-anchor:middle;font-size:12;font-family:var(--font-sans);","Inbound request body",[98,121,124],{"x":116,"y":122,"fill":89,"style":123},"99","text-anchor:middle;font-size:11;font-family:var(--font-sans);","your customers",[91,126],{"x":107,"y":127,"width":109,"height":110,"rx":111,"fill":96,"stroke":112,"style":113},"150",[98,129,131],{"x":116,"y":130,"fill":102,"style":118},"172","Upstream response",[98,133,135],{"x":116,"y":134,"fill":89,"style":123},"189","partner APIs",[137,138],"line",{"x1":139,"y1":140,"x2":141,"y2":142,"stroke":89,"style":143},"210","87","264","112","stroke-width:2;marker-end:url(#pyd2-gate-arrow);",[137,145],{"x1":139,"y1":146,"x2":141,"y2":147,"stroke":89,"style":143},"177","152",[91,149],{"x":150,"y":151,"width":152,"height":153,"rx":111,"fill":154,"stroke":155,"style":113},"270","78","170","104","var(--c-surface-alt)","var(--c-blue)",[98,157,161],{"x":158,"y":159,"fill":102,"style":160},"355","120","text-anchor:middle;font-size:13;font-family:var(--font-sans);","Pydantic model",[98,163,166],{"x":158,"y":164,"fill":89,"style":165},"140","text-anchor:middle;font-size:11;font-family:var(--font-mono);","model_validate_json()",[137,168],{"x1":169,"y1":142,"x2":170,"y2":140,"stroke":89,"style":143},"440","494",[137,172],{"x1":169,"y1":147,"x2":170,"y2":146,"stroke":89,"style":143},[91,174],{"x":175,"y":108,"width":176,"height":110,"rx":111,"fill":96,"stroke":177,"style":113},"500","200","var(--c-teal)",[98,179,181],{"x":180,"y":117,"fill":102,"style":118},"600","Typed object",[98,183,184],{"x":180,"y":122,"fill":89,"style":123},"business logic runs",[91,186],{"x":175,"y":127,"width":176,"height":110,"rx":111,"fill":96,"stroke":187,"style":113},"var(--c-yellow)",[98,189,190],{"x":180,"y":130,"fill":102,"style":118},"ValidationError",[98,192,193],{"x":180,"y":134,"fill":89,"style":123},"422 or alert, never a 500",[98,195,198],{"x":100,"y":196,"fill":89,"style":197},"238","font-size:11;font-family:var(--font-sans);","Nothing untyped reaches your business logic, so downstream code stops writing defensive dict lookups.",[24,200],{},[27,202,204],{"id":203},"the-entry-points-model_validate-and-its-siblings","The Entry Points: model_validate and Its Siblings",[14,206,207,208,211,212,215,216,218,219,222,223,226],{},"In Pydantic v2 the method you reach for is ",[35,209,210],{},"model_validate"," — it replaced v1's ",[35,213,214],{},"parse_obj",". Give it a dict and you get back a validated instance or a ",[35,217,190],{},". For raw bytes or a JSON string straight off the wire, call ",[35,220,221],{},"model_validate_json"," instead: it hands the bytes to ",[35,224,225],{},"pydantic-core",", which parses and validates in a single pass and skips building the intermediate Python dict entirely. That is both faster and stricter, because the parser knows JSON's type system rather than Python's.",[14,228,229,230,233,234,237,238,241,242,244],{},"The third entry point is ",[35,231,232],{},"TypeAdapter",", and builders under-use it. When the payload is a bare array, a ",[35,235,236],{},"dict[str, Decimal]",", or any type that is not a ",[35,239,240],{},"BaseModel"," subclass, ",[35,243,232],{}," gives you the same validation machinery without inventing a wrapper model. Build it once at module scope — constructing one compiles a schema, which costs roughly a millisecond and dwarfs the validation itself.",[53,246,62,251,62,254,62,257,62,264,62,267,62,271,62,276,62,281,62,285,62,289,62,293,62,296,62,299,62,302,62,304,62,310,62,315,62,318,62,322,62,325,62,329,62,332,62,336,62,341,62,343,62,345,62,347,62,352,62,357,62,359,62,362,62,364,62,367,62,371,62,373,62,377],{"viewBox":247,"role":56,"ariaLabelledBy":248,"xmlns":60,"style":61},"0 0 720 310",[249,250],"pyd2-entry-t","pyd2-entry-d",[64,252,253],{"id":249},"Choosing a Pydantic v2 validation entry point",[68,255,256],{"id":250},"A decision tree mapping four input shapes — raw bytes, an existing dict, a JSON array of records, and a non-model type — to model_validate_json, model_validate, and two TypeAdapter calls.",[72,258,74,259,62],{},[76,260,84,262,74],{"id":261,"viewBox":79,"refX":80,"refY":81,"markerWidth":82,"markerHeight":82,"orient":83},"pyd2-entry-arrow",[86,263],{"d":88,"fill":89},[91,265],{"x":93,"y":93,"width":94,"height":266,"fill":96},"310",[98,268,270],{"x":100,"y":269,"fill":102,"style":103},"24","Pick the entry point by the shape of what arrived",[91,272],{"x":100,"y":273,"width":274,"height":275,"rx":111,"fill":154,"stroke":155,"style":113},"136","160","64",[98,277,280],{"x":278,"y":279,"fill":102,"style":118},"96","163","Incoming JSON",[98,282,284],{"x":278,"y":283,"fill":89,"style":123},"181","what shape is it?",[137,286],{"x1":287,"y1":288,"x2":176,"y2":288,"stroke":89,"style":113},"176","168",[137,290],{"x1":176,"y1":291,"x2":176,"y2":150,"stroke":292,"style":113},"66","var(--c-border)",[137,294],{"x1":176,"y1":291,"x2":295,"y2":291,"stroke":292,"style":113},"222",[137,297],{"x1":176,"y1":298,"x2":295,"y2":298,"stroke":292,"style":113},"134",[137,300],{"x1":176,"y1":301,"x2":295,"y2":301,"stroke":292,"style":113},"202",[137,303],{"x1":176,"y1":150,"x2":295,"y2":150,"stroke":292,"style":113},[91,305],{"x":306,"y":307,"width":176,"height":308,"rx":309,"fill":96,"stroke":177,"style":113},"224","42","48","8",[98,311,314],{"x":312,"y":313,"fill":102,"style":118},"324","71","Bytes or str off the wire",[91,316],{"x":306,"y":317,"width":176,"height":308,"rx":309,"fill":96,"stroke":177,"style":113},"110",[98,319,321],{"x":312,"y":320,"fill":102,"style":118},"139","Already a Python dict",[91,323],{"x":306,"y":324,"width":176,"height":308,"rx":309,"fill":96,"stroke":177,"style":113},"178",[98,326,328],{"x":312,"y":327,"fill":102,"style":118},"207","JSON array of records",[91,330],{"x":306,"y":331,"width":176,"height":308,"rx":309,"fill":96,"stroke":177,"style":113},"246",[98,333,335],{"x":312,"y":334,"fill":102,"style":118},"275","Not a BaseModel type",[137,337],{"x1":338,"y1":291,"x2":339,"y2":291,"stroke":89,"style":340},"424","464","stroke-width:2;marker-end:url(#pyd2-entry-arrow);",[137,342],{"x1":338,"y1":298,"x2":339,"y2":298,"stroke":89,"style":340},[137,344],{"x1":338,"y1":301,"x2":339,"y2":301,"stroke":89,"style":340},[137,346],{"x1":338,"y1":150,"x2":339,"y2":150,"stroke":89,"style":340},[91,348],{"x":349,"y":307,"width":350,"height":308,"rx":309,"fill":154,"stroke":292,"style":351},"470","234","stroke-width:1;",[98,353,356],{"x":354,"y":313,"fill":102,"style":355},"482","font-size:11;font-family:var(--font-mono);","M.model_validate_json(raw)",[91,358],{"x":349,"y":317,"width":350,"height":308,"rx":309,"fill":154,"stroke":292,"style":351},[98,360,361],{"x":354,"y":320,"fill":102,"style":355},"M.model_validate(data)",[91,363],{"x":349,"y":324,"width":350,"height":308,"rx":309,"fill":154,"stroke":292,"style":351},[98,365,366],{"x":354,"y":176,"fill":102,"style":355},"TypeAdapter(list[M])",[98,368,370],{"x":354,"y":369,"fill":89,"style":355},"216",".validate_json(raw)",[91,372],{"x":349,"y":331,"width":350,"height":308,"rx":309,"fill":154,"stroke":292,"style":351},[98,374,376],{"x":354,"y":375,"fill":102,"style":355},"268","TypeAdapter(T)",[98,378,380],{"x":354,"y":379,"fill":89,"style":355},"284",".validate_python(v)",[382,383,388],"pre",{"className":384,"code":385,"language":386,"meta":387,"style":387},"language-python shiki shiki-themes github-light github-dark","import os\nimport httpx\nfrom pydantic import BaseModel, TypeAdapter\n\nUPSTREAM_URL = os.getenv(\"UPSTREAM_URL\", \"https:\u002F\u002Fexample.test\u002Fusers\")\nHTTP_TIMEOUT = float(os.getenv(\"HTTP_TIMEOUT_SECONDS\", \"10\"))\n\n\nclass User(BaseModel):\n    id: int\n    email: str\n    is_active: bool = True\n\n\n# Built once at import time — the compiled schema is reused for every call.\nUSER_LIST = TypeAdapter(list[User])\n\n\nasync def fetch_users() -> list[User]:\n    async with httpx.AsyncClient(timeout=HTTP_TIMEOUT) as client:\n        resp = await client.get(UPSTREAM_URL)\n        resp.raise_for_status()\n        # Parse and validate the raw bytes in one pass.\n        return USER_LIST.validate_json(resp.content)\n","python","",[35,389,390,402,410,424,431,457,482,487,492,510,522,531,545,550,555,562,573,578,583,598,628,646,652,658],{"__ignoreMap":387},[391,392,394,398],"span",{"class":137,"line":393},1,[391,395,397],{"class":396},"szBVR","import",[391,399,401],{"class":400},"sVt8B"," os\n",[391,403,405,407],{"class":137,"line":404},2,[391,406,397],{"class":396},[391,408,409],{"class":400}," httpx\n",[391,411,413,416,419,421],{"class":137,"line":412},3,[391,414,415],{"class":396},"from",[391,417,418],{"class":400}," pydantic ",[391,420,397],{"class":396},[391,422,423],{"class":400}," BaseModel, TypeAdapter\n",[391,425,427],{"class":137,"line":426},4,[391,428,430],{"emptyLinePlaceholder":429},true,"\n",[391,432,434,438,441,444,448,451,454],{"class":137,"line":433},5,[391,435,437],{"class":436},"sj4cs","UPSTREAM_URL",[391,439,440],{"class":396}," =",[391,442,443],{"class":400}," os.getenv(",[391,445,447],{"class":446},"sZZnC","\"UPSTREAM_URL\"",[391,449,450],{"class":400},", ",[391,452,453],{"class":446},"\"https:\u002F\u002Fexample.test\u002Fusers\"",[391,455,456],{"class":400},")\n",[391,458,460,463,465,468,471,474,476,479],{"class":137,"line":459},6,[391,461,462],{"class":436},"HTTP_TIMEOUT",[391,464,440],{"class":396},[391,466,467],{"class":436}," float",[391,469,470],{"class":400},"(os.getenv(",[391,472,473],{"class":446},"\"HTTP_TIMEOUT_SECONDS\"",[391,475,450],{"class":400},[391,477,478],{"class":446},"\"10\"",[391,480,481],{"class":400},"))\n",[391,483,485],{"class":137,"line":484},7,[391,486,430],{"emptyLinePlaceholder":429},[391,488,490],{"class":137,"line":489},8,[391,491,430],{"emptyLinePlaceholder":429},[391,493,495,498,502,505,507],{"class":137,"line":494},9,[391,496,497],{"class":396},"class",[391,499,501],{"class":500},"sScJk"," User",[391,503,504],{"class":400},"(",[391,506,240],{"class":500},[391,508,509],{"class":400},"):\n",[391,511,513,516,519],{"class":137,"line":512},10,[391,514,515],{"class":436},"    id",[391,517,518],{"class":400},": ",[391,520,521],{"class":436},"int\n",[391,523,525,528],{"class":137,"line":524},11,[391,526,527],{"class":400},"    email: ",[391,529,530],{"class":436},"str\n",[391,532,534,537,540,542],{"class":137,"line":533},12,[391,535,536],{"class":400},"    is_active: ",[391,538,539],{"class":436},"bool",[391,541,440],{"class":396},[391,543,544],{"class":436}," True\n",[391,546,548],{"class":137,"line":547},13,[391,549,430],{"emptyLinePlaceholder":429},[391,551,553],{"class":137,"line":552},14,[391,554,430],{"emptyLinePlaceholder":429},[391,556,558],{"class":137,"line":557},15,[391,559,561],{"class":560},"sJ8bj","# Built once at import time — the compiled schema is reused for every call.\n",[391,563,565,568,570],{"class":137,"line":564},16,[391,566,567],{"class":436},"USER_LIST",[391,569,440],{"class":396},[391,571,572],{"class":400}," TypeAdapter(list[User])\n",[391,574,576],{"class":137,"line":575},17,[391,577,430],{"emptyLinePlaceholder":429},[391,579,581],{"class":137,"line":580},18,[391,582,430],{"emptyLinePlaceholder":429},[391,584,586,589,592,595],{"class":137,"line":585},19,[391,587,588],{"class":396},"async",[391,590,591],{"class":396}," def",[391,593,594],{"class":500}," fetch_users",[391,596,597],{"class":400},"() -> list[User]:\n",[391,599,601,604,607,610,614,617,619,622,625],{"class":137,"line":600},20,[391,602,603],{"class":396},"    async",[391,605,606],{"class":396}," with",[391,608,609],{"class":400}," httpx.AsyncClient(",[391,611,613],{"class":612},"s4XuR","timeout",[391,615,616],{"class":396},"=",[391,618,462],{"class":436},[391,620,621],{"class":400},") ",[391,623,624],{"class":396},"as",[391,626,627],{"class":400}," client:\n",[391,629,631,634,636,639,642,644],{"class":137,"line":630},21,[391,632,633],{"class":400},"        resp ",[391,635,616],{"class":396},[391,637,638],{"class":396}," await",[391,640,641],{"class":400}," client.get(",[391,643,437],{"class":436},[391,645,456],{"class":400},[391,647,649],{"class":137,"line":648},22,[391,650,651],{"class":400},"        resp.raise_for_status()\n",[391,653,655],{"class":137,"line":654},23,[391,656,657],{"class":560},"        # Parse and validate the raw bytes in one pass.\n",[391,659,661,664,667],{"class":137,"line":660},24,[391,662,663],{"class":396},"        return",[391,665,666],{"class":436}," USER_LIST",[391,668,669],{"class":400},".validate_json(resp.content)\n",[14,671,672,673,676,677,680,681,684,685,689],{},"From that point on there is no ",[35,674,675],{},"data.get(\"id\")"," guesswork. ",[35,678,679],{},"user.id"," is an ",[35,682,683],{},"int",", or the call already failed loudly at the door. Pair this with an ",[18,686,688],{"href":687},"\u002Fgetting-started-with-python-apis-for-builders\u002Fmaking-http-requests-with-requests-library\u002Fhttpx-vs-requests-for-async\u002F","async httpx client"," so the validation cost overlaps nothing on the event loop that matters.",[24,691],{},[27,693,695],{"id":694},"taming-a-messy-upstream-payload","Taming a Messy Upstream Payload",[14,697,698,699,702,703,706],{},"Real upstream JSON is rarely tidy. A partner sends prices as strings, an email with stray whitespace and mixed case, a status as free-form text, and a timestamp as Unix seconds. Aliases, enums, and ",[35,700,701],{},"mode=\"before\""," validators turn that into clean data without a single ",[35,704,705],{},"if"," statement in your business logic.",[382,708,710],{"className":384,"code":709,"language":386,"meta":387,"style":387},"import os\nfrom datetime import datetime\nfrom enum import Enum\nfrom pydantic import BaseModel, ConfigDict, Field, field_validator, ValidationError\n\nDEFAULT_CURRENCY = os.getenv(\"DEFAULT_CURRENCY\", \"usd\")\n\n\nclass Status(str, Enum):\n    active = \"active\"\n    paused = \"paused\"\n\n\nclass Order(BaseModel):\n    model_config = ConfigDict(populate_by_name=True, extra=\"ignore\", frozen=True)\n\n    order_id: int = Field(alias=\"id\")           # map upstream \"id\" -> order_id\n    email: str\n    price_cents: int                            # upstream sends \"12.50\" as a string\n    status: Status                              # constrained to known values\n    created_at: datetime                        # upstream sends Unix seconds\n\n    @field_validator(\"email\")\n    @classmethod\n    def normalize_email(cls, v: str) -> str:\n        return v.strip().lower()\n\n    @field_validator(\"price_cents\", mode=\"before\")\n    @classmethod\n    def dollars_to_cents(cls, v: object) -> int:\n        # \"12.50\" -> 1250. Lax coercion will NOT do this for you.\n        return round(float(v) * 100)\n\n\nmessy = {\n    \"id\": 4815,\n    \"email\": \"  Buyer@Example.COM \",\n    \"price_cents\": \"12.50\",\n    \"status\": \"active\",\n    \"created_at\": 1781740800,   # Unix seconds -> aware datetime, automatically\n}\n\ntry:\n    order = Order.model_validate(messy)\n    print(order.order_id, order.email, order.price_cents)\n    # 4815 buyer@example.com 1250\nexcept ValidationError as exc:\n    print(exc.errors(include_url=False))\n",[35,711,712,718,730,742,753,757,776,780,784,803,813,823,827,831,844,883,887,913,919,929,937,945,949,961,969,991,999,1004,1026,1033,1052,1058,1082,1087,1092,1103,1117,1130,1143,1156,1173,1179,1184,1192,1203,1212,1218,1232],{"__ignoreMap":387},[391,713,714,716],{"class":137,"line":393},[391,715,397],{"class":396},[391,717,401],{"class":400},[391,719,720,722,725,727],{"class":137,"line":404},[391,721,415],{"class":396},[391,723,724],{"class":400}," datetime ",[391,726,397],{"class":396},[391,728,729],{"class":400}," datetime\n",[391,731,732,734,737,739],{"class":137,"line":412},[391,733,415],{"class":396},[391,735,736],{"class":400}," enum ",[391,738,397],{"class":396},[391,740,741],{"class":400}," Enum\n",[391,743,744,746,748,750],{"class":137,"line":426},[391,745,415],{"class":396},[391,747,418],{"class":400},[391,749,397],{"class":396},[391,751,752],{"class":400}," BaseModel, ConfigDict, Field, field_validator, ValidationError\n",[391,754,755],{"class":137,"line":433},[391,756,430],{"emptyLinePlaceholder":429},[391,758,759,762,764,766,769,771,774],{"class":137,"line":459},[391,760,761],{"class":436},"DEFAULT_CURRENCY",[391,763,440],{"class":396},[391,765,443],{"class":400},[391,767,768],{"class":446},"\"DEFAULT_CURRENCY\"",[391,770,450],{"class":400},[391,772,773],{"class":446},"\"usd\"",[391,775,456],{"class":400},[391,777,778],{"class":137,"line":484},[391,779,430],{"emptyLinePlaceholder":429},[391,781,782],{"class":137,"line":489},[391,783,430],{"emptyLinePlaceholder":429},[391,785,786,788,791,793,796,798,801],{"class":137,"line":494},[391,787,497],{"class":396},[391,789,790],{"class":500}," Status",[391,792,504],{"class":400},[391,794,795],{"class":436},"str",[391,797,450],{"class":400},[391,799,800],{"class":500},"Enum",[391,802,509],{"class":400},[391,804,805,808,810],{"class":137,"line":512},[391,806,807],{"class":400},"    active ",[391,809,616],{"class":396},[391,811,812],{"class":446}," \"active\"\n",[391,814,815,818,820],{"class":137,"line":524},[391,816,817],{"class":400},"    paused ",[391,819,616],{"class":396},[391,821,822],{"class":446}," \"paused\"\n",[391,824,825],{"class":137,"line":533},[391,826,430],{"emptyLinePlaceholder":429},[391,828,829],{"class":137,"line":547},[391,830,430],{"emptyLinePlaceholder":429},[391,832,833,835,838,840,842],{"class":137,"line":552},[391,834,497],{"class":396},[391,836,837],{"class":500}," Order",[391,839,504],{"class":400},[391,841,240],{"class":500},[391,843,509],{"class":400},[391,845,846,849,851,854,857,859,862,864,867,869,872,874,877,879,881],{"class":137,"line":557},[391,847,848],{"class":400},"    model_config ",[391,850,616],{"class":396},[391,852,853],{"class":400}," ConfigDict(",[391,855,856],{"class":612},"populate_by_name",[391,858,616],{"class":396},[391,860,861],{"class":436},"True",[391,863,450],{"class":400},[391,865,866],{"class":612},"extra",[391,868,616],{"class":396},[391,870,871],{"class":446},"\"ignore\"",[391,873,450],{"class":400},[391,875,876],{"class":612},"frozen",[391,878,616],{"class":396},[391,880,861],{"class":436},[391,882,456],{"class":400},[391,884,885],{"class":137,"line":564},[391,886,430],{"emptyLinePlaceholder":429},[391,888,889,892,894,896,899,902,904,907,910],{"class":137,"line":575},[391,890,891],{"class":400},"    order_id: ",[391,893,683],{"class":436},[391,895,440],{"class":396},[391,897,898],{"class":400}," Field(",[391,900,901],{"class":612},"alias",[391,903,616],{"class":396},[391,905,906],{"class":446},"\"id\"",[391,908,909],{"class":400},")           ",[391,911,912],{"class":560},"# map upstream \"id\" -> order_id\n",[391,914,915,917],{"class":137,"line":580},[391,916,527],{"class":400},[391,918,530],{"class":436},[391,920,921,924,926],{"class":137,"line":585},[391,922,923],{"class":400},"    price_cents: ",[391,925,683],{"class":436},[391,927,928],{"class":560},"                            # upstream sends \"12.50\" as a string\n",[391,930,931,934],{"class":137,"line":600},[391,932,933],{"class":400},"    status: Status                              ",[391,935,936],{"class":560},"# constrained to known values\n",[391,938,939,942],{"class":137,"line":630},[391,940,941],{"class":400},"    created_at: datetime                        ",[391,943,944],{"class":560},"# upstream sends Unix seconds\n",[391,946,947],{"class":137,"line":648},[391,948,430],{"emptyLinePlaceholder":429},[391,950,951,954,956,959],{"class":137,"line":654},[391,952,953],{"class":500},"    @field_validator",[391,955,504],{"class":400},[391,957,958],{"class":446},"\"email\"",[391,960,456],{"class":400},[391,962,963,966],{"class":137,"line":660},[391,964,965],{"class":500},"    @",[391,967,968],{"class":436},"classmethod\n",[391,970,972,975,978,981,983,986,988],{"class":137,"line":971},25,[391,973,974],{"class":396},"    def",[391,976,977],{"class":500}," normalize_email",[391,979,980],{"class":400},"(cls, v: ",[391,982,795],{"class":436},[391,984,985],{"class":400},") -> ",[391,987,795],{"class":436},[391,989,990],{"class":400},":\n",[391,992,994,996],{"class":137,"line":993},26,[391,995,663],{"class":396},[391,997,998],{"class":400}," v.strip().lower()\n",[391,1000,1002],{"class":137,"line":1001},27,[391,1003,430],{"emptyLinePlaceholder":429},[391,1005,1007,1009,1011,1014,1016,1019,1021,1024],{"class":137,"line":1006},28,[391,1008,953],{"class":500},[391,1010,504],{"class":400},[391,1012,1013],{"class":446},"\"price_cents\"",[391,1015,450],{"class":400},[391,1017,1018],{"class":612},"mode",[391,1020,616],{"class":396},[391,1022,1023],{"class":446},"\"before\"",[391,1025,456],{"class":400},[391,1027,1029,1031],{"class":137,"line":1028},29,[391,1030,965],{"class":500},[391,1032,968],{"class":436},[391,1034,1036,1038,1041,1043,1046,1048,1050],{"class":137,"line":1035},30,[391,1037,974],{"class":396},[391,1039,1040],{"class":500}," dollars_to_cents",[391,1042,980],{"class":400},[391,1044,1045],{"class":436},"object",[391,1047,985],{"class":400},[391,1049,683],{"class":436},[391,1051,990],{"class":400},[391,1053,1055],{"class":137,"line":1054},31,[391,1056,1057],{"class":560},"        # \"12.50\" -> 1250. Lax coercion will NOT do this for you.\n",[391,1059,1061,1063,1066,1068,1071,1074,1077,1080],{"class":137,"line":1060},32,[391,1062,663],{"class":396},[391,1064,1065],{"class":436}," round",[391,1067,504],{"class":400},[391,1069,1070],{"class":436},"float",[391,1072,1073],{"class":400},"(v) ",[391,1075,1076],{"class":396},"*",[391,1078,1079],{"class":436}," 100",[391,1081,456],{"class":400},[391,1083,1085],{"class":137,"line":1084},33,[391,1086,430],{"emptyLinePlaceholder":429},[391,1088,1090],{"class":137,"line":1089},34,[391,1091,430],{"emptyLinePlaceholder":429},[391,1093,1095,1098,1100],{"class":137,"line":1094},35,[391,1096,1097],{"class":400},"messy ",[391,1099,616],{"class":396},[391,1101,1102],{"class":400}," {\n",[391,1104,1106,1109,1111,1114],{"class":137,"line":1105},36,[391,1107,1108],{"class":446},"    \"id\"",[391,1110,518],{"class":400},[391,1112,1113],{"class":436},"4815",[391,1115,1116],{"class":400},",\n",[391,1118,1120,1123,1125,1128],{"class":137,"line":1119},37,[391,1121,1122],{"class":446},"    \"email\"",[391,1124,518],{"class":400},[391,1126,1127],{"class":446},"\"  Buyer@Example.COM \"",[391,1129,1116],{"class":400},[391,1131,1133,1136,1138,1141],{"class":137,"line":1132},38,[391,1134,1135],{"class":446},"    \"price_cents\"",[391,1137,518],{"class":400},[391,1139,1140],{"class":446},"\"12.50\"",[391,1142,1116],{"class":400},[391,1144,1146,1149,1151,1154],{"class":137,"line":1145},39,[391,1147,1148],{"class":446},"    \"status\"",[391,1150,518],{"class":400},[391,1152,1153],{"class":446},"\"active\"",[391,1155,1116],{"class":400},[391,1157,1159,1162,1164,1167,1170],{"class":137,"line":1158},40,[391,1160,1161],{"class":446},"    \"created_at\"",[391,1163,518],{"class":400},[391,1165,1166],{"class":436},"1781740800",[391,1168,1169],{"class":400},",   ",[391,1171,1172],{"class":560},"# Unix seconds -> aware datetime, automatically\n",[391,1174,1176],{"class":137,"line":1175},41,[391,1177,1178],{"class":400},"}\n",[391,1180,1182],{"class":137,"line":1181},42,[391,1183,430],{"emptyLinePlaceholder":429},[391,1185,1187,1190],{"class":137,"line":1186},43,[391,1188,1189],{"class":396},"try",[391,1191,990],{"class":400},[391,1193,1195,1198,1200],{"class":137,"line":1194},44,[391,1196,1197],{"class":400},"    order ",[391,1199,616],{"class":396},[391,1201,1202],{"class":400}," Order.model_validate(messy)\n",[391,1204,1206,1209],{"class":137,"line":1205},45,[391,1207,1208],{"class":436},"    print",[391,1210,1211],{"class":400},"(order.order_id, order.email, order.price_cents)\n",[391,1213,1215],{"class":137,"line":1214},46,[391,1216,1217],{"class":560},"    # 4815 buyer@example.com 1250\n",[391,1219,1221,1224,1227,1229],{"class":137,"line":1220},47,[391,1222,1223],{"class":396},"except",[391,1225,1226],{"class":400}," ValidationError ",[391,1228,624],{"class":396},[391,1230,1231],{"class":400}," exc:\n",[391,1233,1235,1237,1240,1243,1245,1248],{"class":137,"line":1234},48,[391,1236,1208],{"class":436},[391,1238,1239],{"class":400},"(exc.errors(",[391,1241,1242],{"class":612},"include_url",[391,1244,616],{"class":396},[391,1246,1247],{"class":436},"False",[391,1249,481],{"class":400},[14,1251,1252,1253,1255,1256,1258,1259,1262,1263,1266,1267,1270],{},"A ",[35,1254,701],{}," validator runs on the raw value, so it can reshape a string into the type the field expects. That is not optional decoration here: ",[35,1257,1140],{}," is not a valid integer in any Pydantic mode, so without the before-validator the field fails outright. ",[35,1260,1261],{},"status"," is pinned to an enum, so an unknown value from a partner's new feature flag fails immediately instead of flowing into a ",[35,1264,1265],{},"match"," statement that silently falls through. And ",[35,1268,1269],{},"frozen=True"," makes the instance hashable and immutable, which stops a downstream helper from \"fixing\" a price in place.",[53,1272,62,1277,62,1280,62,1283,62,1290,62,1293,62,1297,62,1301,62,1305,62,1309,62,1315,62,1320,62,1323,62,1327,62,1331,62,1335,62,1337,62,1341,62,1343,62,1347,62,1350,62,1352,62,1355,62,1357,62,1359,62,1362,62,1365,62,1369,62,1372,62,1374,62,1376,62,1378,62,1380,62,1383,62,1385,62,1388,62,1390,62,1392,62,1395,62,1397,62,1399,62,1402],{"viewBox":1273,"role":56,"ariaLabelledBy":1274,"xmlns":60,"style":61},"0 0 720 290",[1275,1276],"pyd2-field-t","pyd2-field-d",[64,1278,1279],{"id":1275},"Field-by-field transformation of a messy order payload",[68,1281,1282],{"id":1276},"Four rows showing raw upstream JSON values on the left, the Pydantic mechanism applied in the middle, and the resulting typed attribute on the right.",[72,1284,74,1285,62],{},[76,1286,84,1288,74],{"id":1287,"viewBox":79,"refX":80,"refY":81,"markerWidth":82,"markerHeight":82,"orient":83},"pyd2-field-arrow",[86,1289],{"d":88,"fill":89},[91,1291],{"x":93,"y":93,"width":94,"height":1292,"fill":96},"290",[98,1294,1296],{"x":100,"y":1295,"fill":102,"style":103},"22","What each field looks like before and after",[98,1298,1300],{"x":107,"y":1299,"fill":89,"style":197},"46","Upstream JSON",[98,1302,1304],{"x":1303,"y":1299,"fill":89,"style":197},"278","Pydantic step",[98,1306,1308],{"x":1307,"y":1299,"fill":89,"style":197},"498","Typed attribute",[91,1310],{"x":100,"y":1311,"width":1312,"height":1313,"rx":1314,"fill":96,"stroke":112,"style":113},"58","214","40","6",[98,1316,1319],{"x":1317,"y":1318,"fill":102,"style":355},"28","83","\"id\": 4815",[137,1321],{"x1":350,"y1":151,"x2":150,"y2":151,"stroke":89,"style":1322},"stroke-width:2;marker-end:url(#pyd2-field-arrow);",[91,1324],{"x":1325,"y":1311,"width":1326,"height":1313,"rx":1314,"fill":154,"stroke":292,"style":351},"274","180",[98,1328,1330],{"x":1329,"y":1318,"fill":102,"style":355},"286","Field(alias=\"id\")",[137,1332],{"x1":1333,"y1":151,"x2":1334,"y2":151,"stroke":89,"style":1322},"458","490",[91,1336],{"x":170,"y":1311,"width":139,"height":1313,"rx":1314,"fill":96,"stroke":177,"style":113},[98,1338,1340],{"x":1339,"y":1318,"fill":102,"style":355},"506","order_id: int = 4815",[91,1342],{"x":100,"y":317,"width":1312,"height":1313,"rx":1314,"fill":96,"stroke":112,"style":113},[98,1344,1346],{"x":1317,"y":1345,"fill":102,"style":355},"135","\"  Buyer@Ex.COM \"",[137,1348],{"x1":350,"y1":1349,"x2":150,"y2":1349,"stroke":89,"style":1322},"130",[91,1351],{"x":1325,"y":317,"width":1326,"height":1313,"rx":1314,"fill":154,"stroke":292,"style":351},[98,1353,1354],{"x":1329,"y":1345,"fill":102,"style":355},"field_validator",[137,1356],{"x1":1333,"y1":1349,"x2":1334,"y2":1349,"stroke":89,"style":1322},[91,1358],{"x":170,"y":317,"width":139,"height":1313,"rx":1314,"fill":96,"stroke":177,"style":113},[98,1360,1361],{"x":1339,"y":1345,"fill":102,"style":355},"email = \"buyer@ex.com\"",[91,1363],{"x":100,"y":1364,"width":1312,"height":1313,"rx":1314,"fill":96,"stroke":112,"style":113},"162",[98,1366,1368],{"x":1317,"y":1367,"fill":102,"style":355},"187","\"price_cents\": \"12.50\"",[137,1370],{"x1":350,"y1":1371,"x2":150,"y2":1371,"stroke":89,"style":1322},"182",[91,1373],{"x":1325,"y":1364,"width":1326,"height":1313,"rx":1314,"fill":154,"stroke":292,"style":351},[98,1375,701],{"x":1329,"y":1367,"fill":102,"style":355},[137,1377],{"x1":1333,"y1":1371,"x2":1334,"y2":1371,"stroke":89,"style":1322},[91,1379],{"x":170,"y":1364,"width":139,"height":1313,"rx":1314,"fill":96,"stroke":177,"style":113},[98,1381,1382],{"x":1339,"y":1367,"fill":102,"style":355},"price_cents = 1250",[91,1384],{"x":100,"y":1312,"width":1312,"height":1313,"rx":1314,"fill":96,"stroke":112,"style":113},[98,1386,1166],{"x":1317,"y":1387,"fill":102,"style":355},"239",[137,1389],{"x1":350,"y1":350,"x2":150,"y2":350,"stroke":89,"style":1322},[91,1391],{"x":1325,"y":1312,"width":1326,"height":1313,"rx":1314,"fill":154,"stroke":292,"style":351},[98,1393,1394],{"x":1329,"y":1387,"fill":102,"style":355},"datetime coercion",[137,1396],{"x1":1333,"y1":350,"x2":1334,"y2":350,"stroke":89,"style":1322},[91,1398],{"x":170,"y":1312,"width":139,"height":1313,"rx":1314,"fill":96,"stroke":177,"style":113},[98,1400,1401],{"x":1339,"y":1387,"fill":102,"style":355},"2026-06-18 UTC",[98,1403,1405],{"x":100,"y":1404,"fill":89,"style":197},"277","Every transformation lives on the model, so the calling code never re-checks a value.",[24,1407],{},[27,1409,1411],{"id":1410},"strict-mode-know-exactly-what-gets-coerced","Strict Mode: Know Exactly What Gets Coerced",[14,1413,1414,1415,1418,1419,1421,1422,1418,1425,1428,1429,1418,1432,1434,1435,1437,1438,1440],{},"The most expensive Pydantic bug is not a rejected payload — it is an accepted one. Lax mode is the default, and it will happily turn the string ",[35,1416,1417],{},"\"42\""," into ",[35,1420,307],{},", the float ",[35,1423,1424],{},"12.0",[35,1426,1427],{},"12",", and the integer ",[35,1430,1431],{},"1",[35,1433,861],{},". That is usually what you want from a sloppy partner. It is almost never what you want from a paying customer's request body, where a ",[35,1436,1431],{}," in a ",[35,1439,539],{}," field means their client is misconfigured and you would rather tell them now than bill them for a plan they did not pick.",[14,1442,1443,1444,1446,1447,1450,1451,1454,1455,1457,1458,1461,1462,1465,1466,1469],{},"The conversion table also differs between Python mode and JSON mode. ",[35,1445,221],{}," in strict mode still accepts ",[35,1448,1449],{},"\"2026-06-18\""," for a ",[35,1452,1453],{},"datetime"," field, because JSON has no datetime type and a string is the only representation available. ",[35,1456,210],{}," in strict mode rejects it. Turn strict on per field with ",[35,1459,1460],{},"Field(strict=True)",", per model with ",[35,1463,1464],{},"ConfigDict(strict=True)",", or per call with ",[35,1467,1468],{},"Model.model_validate(data, strict=True)"," — the per-call form is the one to reach for when the same model serves a tolerant upstream reader and a strict inbound writer.",[53,1471,62,1476,62,1479,62,1482,62,1485,62,1488,62,1491,62,1495,62,1499,62,1503,62,1507,62,1512,62,1515,62,1520,62,1525,62,1529,62,1532,62,1536,62,1539,62,1542,62,1545,62,1548,62,1550,62,1552,62,1554,62,1556,62,1558,62,1561,62,1563,62,1565,62,1567,62,1569,62,1571,62,1573,62,1576,62,1579,62,1581,62,1583,62,1585,62,1587,62,1589,62,1593,62,1595,62,1597,62,1599,62,1601,62,1603,62,1605],{"viewBox":1472,"role":56,"ariaLabelledBy":1473,"xmlns":60,"style":61},"0 0 720 300",[1474,1475],"pyd2-strict-t","pyd2-strict-d",[64,1477,1478],{"id":1474},"Pydantic v2 coercion matrix: lax versus strict",[68,1480,1481],{"id":1475},"A matrix of five input-to-field-type conversions showing which pass in lax mode, in strict Python mode, and in strict JSON mode.",[91,1483],{"x":93,"y":93,"width":94,"height":1484,"fill":96},"300",[98,1486,1487],{"x":100,"y":269,"fill":102,"style":103},"What each mode accepts",[98,1489,1490],{"x":1317,"y":275,"fill":89,"style":197},"Input value to field type",[98,1492,1494],{"x":1493,"y":275,"fill":89,"style":123},"377","Lax (default)",[98,1496,1498],{"x":1497,"y":275,"fill":89,"style":123},"511","Strict Python",[98,1500,1502],{"x":1501,"y":275,"fill":89,"style":123},"643","Strict JSON",[137,1504],{"x1":100,"y1":1505,"x2":1506,"y2":1505,"stroke":292,"style":351},"74","704",[91,1508],{"x":100,"y":1509,"width":1510,"height":1511,"fill":154},"116","688","38",[91,1513],{"x":100,"y":1514,"width":1510,"height":1511,"fill":154},"192",[98,1516,1519],{"x":1317,"y":1517,"fill":102,"style":1518},"102","font-size:12;font-family:var(--font-mono);","\"42\" → int",[1521,1522],"circle",{"cx":1523,"cy":1524,"r":81,"fill":177},"347","94",[98,1526,1528],{"x":1527,"y":1517,"fill":102,"style":123},"385","pass",[1521,1530],{"cx":1531,"cy":1524,"r":81,"fill":112},"481",[98,1533,1535],{"x":1534,"y":1517,"fill":102,"style":123},"519","fail",[1521,1537],{"cx":1538,"cy":1524,"r":81,"fill":112},"613",[98,1540,1535],{"x":1541,"y":1517,"fill":102,"style":123},"651",[98,1543,1544],{"x":1317,"y":164,"fill":102,"style":1518},"\"12.50\" → int",[1521,1546],{"cx":1523,"cy":1547,"r":81,"fill":112},"132",[98,1549,1535],{"x":1527,"y":164,"fill":102,"style":123},[1521,1551],{"cx":1531,"cy":1547,"r":81,"fill":112},[98,1553,1535],{"x":1534,"y":164,"fill":102,"style":123},[1521,1555],{"cx":1538,"cy":1547,"r":81,"fill":112},[98,1557,1535],{"x":1541,"y":164,"fill":102,"style":123},[98,1559,1560],{"x":1317,"y":324,"fill":102,"style":1518},"12.0 → int",[1521,1562],{"cx":1523,"cy":152,"r":81,"fill":177},[98,1564,1528],{"x":1527,"y":324,"fill":102,"style":123},[1521,1566],{"cx":1531,"cy":152,"r":81,"fill":112},[98,1568,1535],{"x":1534,"y":324,"fill":102,"style":123},[1521,1570],{"cx":1538,"cy":152,"r":81,"fill":112},[98,1572,1535],{"x":1541,"y":324,"fill":102,"style":123},[98,1574,1575],{"x":1317,"y":369,"fill":102,"style":1518},"1 → bool",[1521,1577],{"cx":1523,"cy":1578,"r":81,"fill":177},"208",[98,1580,1528],{"x":1527,"y":369,"fill":102,"style":123},[1521,1582],{"cx":1531,"cy":1578,"r":81,"fill":112},[98,1584,1535],{"x":1534,"y":369,"fill":102,"style":123},[1521,1586],{"cx":1538,"cy":1578,"r":81,"fill":112},[98,1588,1535],{"x":1541,"y":369,"fill":102,"style":123},[98,1590,1592],{"x":1317,"y":1591,"fill":102,"style":1518},"254","\"2026-06-18\" → datetime",[1521,1594],{"cx":1523,"cy":331,"r":81,"fill":177},[98,1596,1528],{"x":1527,"y":1591,"fill":102,"style":123},[1521,1598],{"cx":1531,"cy":331,"r":81,"fill":112},[98,1600,1535],{"x":1534,"y":1591,"fill":102,"style":123},[1521,1602],{"cx":1538,"cy":331,"r":81,"fill":177},[98,1604,1528],{"x":1541,"y":1591,"fill":102,"style":123},[98,1606,1607],{"x":100,"y":1329,"fill":89,"style":197},"Strict JSON keeps date strings because JSON has no datetime type — that asymmetry surprises people.",[24,1609],{},[27,1611,1613],{"id":1612},"errors-that-tell-you-what-broke","Errors That Tell You What Broke",[14,1615,1616,1617,1620,1621,1624,1625,1628,1629,450,1632,450,1635,450,1638,1641],{},"When validation fails, ",[35,1618,1619],{},"ValidationError.errors()"," returns a structured list: each entry names the location, the failure type, the message, and the offending input. Branch on ",[35,1622,1623],{},"type",", never on ",[35,1626,1627],{},"msg"," — the type codes (",[35,1630,1631],{},"int_parsing",[35,1633,1634],{},"missing",[35,1636,1637],{},"enum",[35,1639,1640],{},"string_too_short",") are stable across releases while the human message is not.",[382,1643,1645],{"className":384,"code":1644,"language":386,"meta":387,"style":387},"from pydantic import ValidationError\n\ntry:\n    Order.model_validate({\"id\": \"not-an-int\", \"email\": \"x\", \"status\": \"weird\"})\nexcept ValidationError as exc:\n    for err in exc.errors(include_url=False, include_input=False):\n        match err[\"type\"]:\n            case \"missing\":\n                print(\"upstream dropped a field:\", err[\"loc\"])\n            case \"enum\":\n                print(\"unknown enum value at:\", err[\"loc\"])\n            case _:\n                print(err[\"loc\"], err[\"type\"], err[\"msg\"])\n",[35,1646,1647,1658,1662,1668,1702,1712,1743,1757,1767,1786,1795,1810,1817],{"__ignoreMap":387},[391,1648,1649,1651,1653,1655],{"class":137,"line":393},[391,1650,415],{"class":396},[391,1652,418],{"class":400},[391,1654,397],{"class":396},[391,1656,1657],{"class":400}," ValidationError\n",[391,1659,1660],{"class":137,"line":404},[391,1661,430],{"emptyLinePlaceholder":429},[391,1663,1664,1666],{"class":137,"line":412},[391,1665,1189],{"class":396},[391,1667,990],{"class":400},[391,1669,1670,1673,1675,1677,1680,1682,1684,1686,1689,1691,1694,1696,1699],{"class":137,"line":426},[391,1671,1672],{"class":400},"    Order.model_validate({",[391,1674,906],{"class":446},[391,1676,518],{"class":400},[391,1678,1679],{"class":446},"\"not-an-int\"",[391,1681,450],{"class":400},[391,1683,958],{"class":446},[391,1685,518],{"class":400},[391,1687,1688],{"class":446},"\"x\"",[391,1690,450],{"class":400},[391,1692,1693],{"class":446},"\"status\"",[391,1695,518],{"class":400},[391,1697,1698],{"class":446},"\"weird\"",[391,1700,1701],{"class":400},"})\n",[391,1703,1704,1706,1708,1710],{"class":137,"line":433},[391,1705,1223],{"class":396},[391,1707,1226],{"class":400},[391,1709,624],{"class":396},[391,1711,1231],{"class":400},[391,1713,1714,1717,1720,1723,1726,1728,1730,1732,1734,1737,1739,1741],{"class":137,"line":459},[391,1715,1716],{"class":396},"    for",[391,1718,1719],{"class":400}," err ",[391,1721,1722],{"class":396},"in",[391,1724,1725],{"class":400}," exc.errors(",[391,1727,1242],{"class":612},[391,1729,616],{"class":396},[391,1731,1247],{"class":436},[391,1733,450],{"class":400},[391,1735,1736],{"class":612},"include_input",[391,1738,616],{"class":396},[391,1740,1247],{"class":436},[391,1742,509],{"class":400},[391,1744,1745,1748,1751,1754],{"class":137,"line":484},[391,1746,1747],{"class":396},"        match",[391,1749,1750],{"class":400}," err[",[391,1752,1753],{"class":446},"\"type\"",[391,1755,1756],{"class":400},"]:\n",[391,1758,1759,1762,1765],{"class":137,"line":489},[391,1760,1761],{"class":396},"            case",[391,1763,1764],{"class":446}," \"missing\"",[391,1766,990],{"class":400},[391,1768,1769,1772,1774,1777,1780,1783],{"class":137,"line":494},[391,1770,1771],{"class":436},"                print",[391,1773,504],{"class":400},[391,1775,1776],{"class":446},"\"upstream dropped a field:\"",[391,1778,1779],{"class":400},", err[",[391,1781,1782],{"class":446},"\"loc\"",[391,1784,1785],{"class":400},"])\n",[391,1787,1788,1790,1793],{"class":137,"line":512},[391,1789,1761],{"class":396},[391,1791,1792],{"class":446}," \"enum\"",[391,1794,990],{"class":400},[391,1796,1797,1799,1801,1804,1806,1808],{"class":137,"line":524},[391,1798,1771],{"class":436},[391,1800,504],{"class":400},[391,1802,1803],{"class":446},"\"unknown enum value at:\"",[391,1805,1779],{"class":400},[391,1807,1782],{"class":446},[391,1809,1785],{"class":400},[391,1811,1812,1814],{"class":137,"line":533},[391,1813,1761],{"class":396},[391,1815,1816],{"class":400}," _:\n",[391,1818,1819,1821,1824,1826,1829,1831,1833,1836],{"class":137,"line":547},[391,1820,1771],{"class":436},[391,1822,1823],{"class":400},"(err[",[391,1825,1782],{"class":446},[391,1827,1828],{"class":400},"], err[",[391,1830,1753],{"class":446},[391,1832,1828],{"class":400},[391,1834,1835],{"class":446},"\"msg\"",[391,1837,1785],{"class":400},[14,1839,1840,1841,1844,1845,1849,1850,1854,1855,1859],{},"Pass ",[35,1842,1843],{},"include_input=False"," before anything reaches your logs. The default error payload embeds the raw offending value, which on an inbound request body is exactly where a customer's email address or API key ends up in plain text — a real problem once you ship ",[18,1846,1848],{"href":1847},"\u002Fscaling-and-operating-production-python-apis\u002Fmonitoring-and-logging-python-apis\u002Fstructured-logging-with-structlog\u002F","structured logging with structlog"," and fan those events out to a third-party log sink. Inside ",[18,1851,1853],{"href":1852},"\u002Fgetting-started-with-python-apis-for-builders\u002Fsetting-up-fastapi\u002F","FastAPI"," this whole path is automatic for inbound bodies: a model on the handler signature turns a malformed body into a 422 with the same structured detail, and the model's JSON schema flows straight into your docs, which is why ",[18,1856,1858],{"href":1857},"\u002Fgetting-started-with-python-apis-for-builders\u002Fdocumenting-apis-with-openapi\u002Fcustomizing-fastapi-openapi-schema\u002F","customizing the FastAPI OpenAPI schema"," starts with getting these models right.",[53,1861,62,1865,62,1868,62,1871,62,1873,62,1876,62,1879,62,1884,62,1888,62,1892,62,1896,62,1900,62,1903,62,1907,62,1911,62,1914,62,1917,62,1920,62,1923,62,1926,62,1929],{"viewBox":55,"role":56,"ariaLabelledBy":1862,"xmlns":60,"style":61},[1863,1864],"pyd2-err-t","pyd2-err-d",[64,1866,1867],{"id":1863},"Anatomy of a single ValidationError entry",[68,1869,1870],{"id":1864},"An annotated error dictionary showing the type, loc, msg and input keys, each labelled with how a production service should treat it.",[91,1872],{"x":93,"y":93,"width":94,"height":95,"fill":96},[98,1874,1875],{"x":100,"y":269,"fill":102,"style":103},"How to read one entry from errors()",[91,1877],{"x":100,"y":1313,"width":1878,"height":109,"rx":309,"fill":154,"stroke":292,"style":351},"400",[98,1880,1883],{"x":1881,"y":1882,"fill":89,"style":1518},"32","68","{",[98,1885,1887],{"x":1881,"y":1886,"fill":102,"style":1518},"98","  \"type\": \"int_parsing\",",[98,1889,1891],{"x":1881,"y":1890,"fill":102,"style":1518},"128","  \"loc\": (\"order_id\",),",[98,1893,1895],{"x":1881,"y":1894,"fill":102,"style":1518},"158","  \"msg\": \"Input should be ...\",",[98,1897,1899],{"x":1881,"y":1898,"fill":102,"style":1518},"188","  \"input\": \"not-an-int\",",[98,1901,1902],{"x":1881,"y":369,"fill":89,"style":1518},"}",[137,1904],{"x1":1905,"y1":1524,"x2":1906,"y2":1524,"stroke":155,"style":113},"416","438",[98,1908,1910],{"x":1909,"y":1886,"fill":102,"style":197},"444","stable code — branch on this",[137,1912],{"x1":1905,"y1":1913,"x2":1906,"y2":1913,"stroke":155,"style":113},"124",[98,1915,1916],{"x":1909,"y":1890,"fill":102,"style":197},"path into the payload",[137,1918],{"x1":1905,"y1":1919,"x2":1906,"y2":1919,"stroke":177,"style":113},"154",[98,1921,1922],{"x":1909,"y":1894,"fill":102,"style":197},"safe to return to the caller",[137,1924],{"x1":1905,"y1":1925,"x2":1906,"y2":1925,"stroke":112,"style":113},"184",[98,1927,1928],{"x":1909,"y":1898,"fill":102,"style":197},"raw value — strip before logging",[98,1930,1932],{"x":100,"y":1931,"fill":89,"style":197},"250","errors(include_input=False, include_url=False) gives you the two safe keys and nothing else.",[1934,1935,1937],"h3",{"id":1936},"failure-modes-that-bite-in-production","Failure Modes That Bite in Production",[1939,1940,1941,1949,1973,1986,1999],"ol",{},[1942,1943,1944,1948],"li",{},[1945,1946,1947],"strong",{},"Defining a model inside a request handler."," Schema construction costs roughly a millisecond — several hundred times a validation. Module scope only.",[1942,1950,1951,1957,1958,1961,1962,1965,1966,1969,1970,1972],{},[1945,1952,1953,1956],{},[35,1954,1955],{},"extra=\"ignore\""," on your own inbound API."," A customer misspells ",[35,1959,1960],{},"currency"," as ",[35,1963,1964],{},"curency",", you silently default to USD, and they open a billing dispute. Use ",[35,1967,1968],{},"extra=\"forbid\""," for requests you own and ",[35,1971,1955],{}," for upstream responses you do not.",[1942,1974,1975,1981,1982,1985],{},[1945,1976,1977,1980],{},[35,1978,1979],{},"str | None"," with no default is still required."," Nullable is not optional. Write ",[35,1983,1984],{},"str | None = None"," when the field may be absent.",[1942,1987,1988,1998],{},[1945,1989,1990,1991,1994,1995],{},"A validator raising something other than ",[35,1992,1993],{},"ValueError"," or ",[35,1996,1997],{},"AssertionError"," escapes as-is, so your 422 becomes a 500 and your error rate dashboard lights up for the wrong reason.",[1942,2000,2001,2004,2005,2008],{},[1945,2002,2003],{},"Mixed timezone awareness."," Unix integers coerce to aware UTC datetimes; naive ISO strings stay naive. Comparing the two raises ",[35,2006,2007],{},"TypeError"," weeks later. Normalize in a validator.",[24,2010],{},[27,2012,2014],{"id":2013},"configuration-and-cost-what-pydantic-core-actually-buys","Configuration and Cost: What pydantic-core Actually Buys",[14,2016,2017,2018,2020,2021,2024,2025,2028],{},"Pydantic v2 moved validation into ",[35,2019,225],{},", a Rust engine, and the numbers below are what that looks like on a 12-field order model with a 2 KB payload, timed with ",[35,2022,2023],{},"pytest-benchmark"," on CPython 3.12. The jump from v1 is roughly threefold, and skipping ",[35,2026,2027],{},"json.loads"," saves another 37%.",[53,2030,62,2035,62,2038,62,2041,62,2043,62,2046,62,2050,62,2054,62,2059,62,2064,62,2067,62,2071,62,2075,62,2077,62,2080,62,2084,62,2087,62,2091,62,2095],{"viewBox":2031,"role":56,"ariaLabelledBy":2032,"xmlns":60,"style":61},"0 0 720 250",[2033,2034],"pyd2-perf-t","pyd2-perf-d",[64,2036,2037],{"id":2033},"Microseconds to validate a 2 KB order payload",[68,2039,2040],{"id":2034},"Bar chart comparing four validation paths: Pydantic v1 parse_obj at 14.6 microseconds, v2 model_validate at 5.1, v2 model_validate_json at 3.2, and a batched TypeAdapter at 2.4 per record.",[91,2042],{"x":93,"y":93,"width":94,"height":1931,"fill":96},[98,2044,2045],{"x":100,"y":101,"fill":102,"style":103},"Microseconds per 2 KB order payload (lower is better)",[137,2047],{"x1":176,"y1":2048,"x2":176,"y2":2049,"stroke":292,"style":351},"44","212",[91,2051],{"x":176,"y":2052,"width":1906,"height":269,"rx":2053,"fill":112},"52","3",[98,2055,2058],{"x":109,"y":2056,"fill":102,"style":2057},"69","text-anchor:end;font-size:12;font-family:var(--font-sans);","v1 parse_obj",[98,2060,2063],{"x":2061,"y":2056,"fill":102,"style":2062},"646","font-size:12;font-family:var(--font-sans);","14.6",[91,2065],{"x":176,"y":1524,"width":2066,"height":269,"rx":2053,"fill":155},"153",[98,2068,2070],{"x":109,"y":2069,"fill":102,"style":2057},"111","v2 model_validate",[98,2072,2074],{"x":2073,"y":2069,"fill":102,"style":2062},"361","5.1",[91,2076],{"x":176,"y":273,"width":278,"height":269,"rx":2053,"fill":177},[98,2078,2079],{"x":109,"y":2066,"fill":102,"style":2057},"v2 validate_json",[98,2081,2083],{"x":2082,"y":2066,"fill":102,"style":2062},"304","3.2",[91,2085],{"x":176,"y":324,"width":2086,"height":269,"rx":2053,"fill":187},"72",[98,2088,2090],{"x":109,"y":2089,"fill":102,"style":2057},"195","TypeAdapter batch",[98,2092,2094],{"x":2093,"y":2089,"fill":102,"style":2062},"280","2.4",[98,2096,2097],{"x":100,"y":350,"fill":89,"style":197},"CPython 3.12, 12-field model, 500-record batch for the last bar. Schema build excluded: it happens once at import.",[14,2099,2100,2101,2105],{},"Turn that into money. A service handling 1M requests a month that validates one inbound body and two upstream responses per request burns about 9.6 CPU-seconds a month on validation. On a $7 instance with roughly 2.6M CPU-seconds available, that is four ten-thousandths of a percent of what you already pay. Even the v1 path costs only 44 seconds. Validation has never been the reason your margin is thin — ",[18,2102,2104],{"href":2103},"\u002Fbuilding-monetizing-api-driven-micro-saas\u002Fdesigning-api-pricing-tiers\u002Fcalculating-cost-per-api-request\u002F","calculating cost per API request"," will point you at egress and database round-trips instead.",[14,2107,2108],{},"The configuration that matters more than speed is which knobs you flip per direction:",[2110,2111,2112,2128],"table",{},[2113,2114,2115],"thead",{},[2116,2117,2118,2122,2125],"tr",{},[2119,2120,2121],"th",{},"Setting",[2119,2123,2124],{},"Inbound requests",[2119,2126,2127],{},"Upstream responses",[2129,2130,2131,2147,2162,2176,2190],"tbody",{},[2116,2132,2133,2138,2143],{},[2134,2135,2136],"td",{},[35,2137,866],{},[2134,2139,2140],{},[35,2141,2142],{},"\"forbid\"",[2134,2144,2145],{},[35,2146,871],{},[2116,2148,2149,2154,2158],{},[2134,2150,2151],{},[35,2152,2153],{},"strict",[2134,2155,2156],{},[35,2157,861],{},[2134,2159,2160],{},[35,2161,1247],{},[2116,2163,2164,2168,2172],{},[2134,2165,2166],{},[35,2167,856],{},[2134,2169,2170],{},[35,2171,861],{},[2134,2173,2174],{},[35,2175,861],{},[2116,2177,2178,2182,2186],{},[2134,2179,2180],{},[35,2181,876],{},[2134,2183,2184],{},[35,2185,1247],{},[2134,2187,2188],{},[35,2189,861],{},[2116,2191,2192,2197,2201],{},[2134,2193,2194],{},[35,2195,2196],{},"str_strip_whitespace",[2134,2198,2199],{},[35,2200,861],{},[2134,2202,2203],{},[35,2204,861],{},[14,2206,2207,2208,2212,2213,2216,2217,2221,2222,2226],{},"Forbid extras on the requests you own so client typos surface as a 422 instead of a silent default. Ignore them upstream so a partner adding a field on Tuesday does not page you — that tolerance is what lets you keep ",[18,2209,2211],{"href":2210},"\u002Fscaling-and-operating-production-python-apis\u002Fversioning-and-evolving-public-apis\u002F","versioning and evolving public APIs"," on your own schedule rather than theirs. Freeze upstream models because nothing should mutate a record you did not create, and lock strict mode on inbound only, where a coerced ",[35,2214,2215],{},"\"1\""," means a broken client integration you want to hear about today. Pin these choices with tests that feed deliberately broken payloads through the model — ",[18,2218,2220],{"href":2219},"\u002Fscaling-and-operating-production-python-apis\u002Ftesting-python-apis-with-pytest\u002Fmocking-external-apis-with-respx\u002F","mocking external APIs with respx"," makes replaying a partner's malformed response a two-line fixture. The same discipline is what keeps ",[18,2223,2225],{"href":2224},"\u002Fautomating-side-hustle-operations-with-apis\u002Fautomating-ai-workflows-with-python-apis\u002F","AI workflows"," sane, where a model returns JSON-shaped text that is right 97% of the time and needs a validated retry for the rest.",[24,2228],{},[27,2230,2232],{"id":2231},"builder-verdict","Builder Verdict",[14,2234,2235,2236,2238,2239,450,2241,2243,2244,2246,2247,2249,2250,2252,2253,2256,2257,2259],{},"Put a Pydantic v2 model on every JSON boundary your service touches, and configure it differently on each side. For upstream responses: ",[35,2237,221],{}," on the raw bytes, ",[35,2240,1955],{},[35,2242,1269],{},", lax coercion, and ",[35,2245,701],{}," validators to normalize the mess. For inbound bodies: ",[35,2248,1968],{},", strict mode, and FastAPI's automatic 422. Build every ",[35,2251,232],{}," at module scope, strip ",[35,2254,2255],{},"input"," out of errors before logging, and branch on error ",[35,2258,1623],{}," codes rather than messages. The performance argument is settled — 3.2 microseconds is not a line item on any invoice — so the only remaining question is whether you would rather find a schema change at the door or three tables deep in your database. The few lines a model costs you are repaid the first time a partner silently renames a field and your service rejects the payload instead of writing a null price to an order you are about to bill.",[27,2261,2263],{"id":2262},"faq","FAQ",[14,2265,2266,2269,2270,2272,2273,2275,2276,2278,2279,2282,2283,2285,2286,2288],{},[1945,2267,2268],{},"What replaced parse_obj from Pydantic v1?","\nUse ",[35,2271,210],{}," for a dict and ",[35,2274,221],{}," for raw JSON bytes or a string. ",[35,2277,214],{}," and ",[35,2280,2281],{},"parse_raw"," are deprecated. Prefer ",[35,2284,221],{}," for wire data because it parses and validates in one pass, which measures about 37% faster than calling ",[35,2287,2027],{}," first.",[14,2290,2291,2294],{},[1945,2292,2293],{},"Does adding validation everywhere hurt my margin at scale?","\nNo. At 1M requests a month with three validations per request you spend under ten CPU-seconds total, which rounds to nothing on a $7 instance. The cost that matters is the corrupted record you bill a customer for, and that is the cost validation removes.",[14,2296,2297,2300,2301,2303,2304,2306,2307,2309],{},[1945,2298,2299],{},"How do I stop a partner's schema change from breaking my service overnight?","\nSet ",[35,2302,1955],{}," on upstream models so added fields are harmless, constrain only the fields you actually use, and alert on ",[35,2305,190],{}," counts rather than failing the whole job. A removed field then shows up as a ",[35,2308,1634],{}," error code in your metrics before a customer notices.",[14,2311,2312,2315,2316,2319,2320,2323,2324,2278,2326,2329],{},[1945,2313,2314],{},"Is it safe to log ValidationError contents?","\nNot by default. ",[35,2317,2318],{},"errors()"," includes the raw offending input, which on inbound requests can be an email, a token, or card metadata. Call ",[35,2321,2322],{},"errors(include_input=False, include_url=False)"," and log the ",[35,2325,1623],{},[35,2327,2328],{},"loc"," keys only, then keep the full payload out of any third-party log sink.",[14,2331,2332,2335],{},[1945,2333,2334],{},"Should I use one model for both my request body and my database row?","\nNo. Keep the boundary model separate from your persistence model. The boundary model changes when a customer-facing contract changes, and a shared model quietly couples an API version bump to a schema migration, which is the fastest way to turn a small deprecation into a breaking release.",[27,2337,2339],{"id":2338},"related","Related",[14,2341,2342],{},[1945,2343,2344],{},"Same track:",[2346,2347,2348,2353,2359,2365],"ul",{},[1942,2349,2350,2352],{},[18,2351,21],{"href":20}," — the parent guide covering defensive parsing end to end.",[1942,2354,2355,2358],{},[18,2356,2357],{"href":49},"Handling Large JSON Payloads with Streaming"," — validating record by record when the document will not fit in memory.",[1942,2360,2361,2364],{},[18,2362,2363],{"href":44},"Debugging 401 Unauthorized API Errors"," — the auth half of the same request boundary.",[1942,2366,2367,2370],{},[18,2368,2369],{"href":1852},"Setting Up FastAPI"," — where these models become automatic 422 responses and OpenAPI schemas.",[14,2372,2373],{},[1945,2374,2375],{},"Other tracks:",[2346,2377,2378,2385],{},[1942,2379,2380,2384],{},[18,2381,2383],{"href":2382},"\u002Fscaling-and-operating-production-python-apis\u002Ftesting-python-apis-with-pytest\u002F","Testing Python APIs with pytest"," — pinning validation behaviour with deliberately broken payloads.",[1942,2386,2387,2391],{},[18,2388,2390],{"href":2389},"\u002Fautomating-side-hustle-operations-with-apis\u002Fprocessing-webhooks-with-python\u002Fverifying-stripe-webhook-signatures\u002F","Verifying Stripe Webhook Signatures"," — the one payload you must verify before you validate.",[2393,2394,2395],"style",{},"html pre.shiki code .szBVR, html code.shiki .szBVR{--shiki-default:#D73A49;--shiki-dark:#F97583}html pre.shiki code .sVt8B, html code.shiki .sVt8B{--shiki-default:#24292E;--shiki-dark:#E1E4E8}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 .sScJk, html code.shiki .sScJk{--shiki-default:#6F42C1;--shiki-dark:#B392F0}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}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);}",{"title":387,"searchDepth":404,"depth":404,"links":2397},[2398,2399,2400,2401,2402,2405,2406,2407,2408],{"id":29,"depth":404,"text":30},{"id":203,"depth":404,"text":204},{"id":694,"depth":404,"text":695},{"id":1410,"depth":404,"text":1411},{"id":1612,"depth":404,"text":1613,"children":2403},[2404],{"id":1936,"depth":412,"text":1937},{"id":2013,"depth":404,"text":2014},{"id":2231,"depth":404,"text":2232},{"id":2262,"depth":404,"text":2263},{"id":2338,"depth":404,"text":2339},"Validate upstream and inbound JSON with Pydantic v2: model_validate, TypeAdapter, strict-mode coercion rules, alias mapping, error triage and per-call timings.","md",{"pageTitle":2412,"type":2413,"datePublished":2414,"dateModified":2415},"Validating JSON with Pydantic v2 in Production Python APIs","article","2026-06-18","2026-07-23","\u002Fgetting-started-with-python-apis-for-builders\u002Fparsing-json-responses\u002Fvalidating-json-with-pydantic-v2",{"title":5,"description":2409},"getting-started-with-python-apis-for-builders\u002Fparsing-json-responses\u002Fvalidating-json-with-pydantic-v2\u002Findex","BcPSgpgPUt2EMK-OO1l25yPwggyUbCbhxjGifbxKrq0",{"@context":2421,"@type":2422,"mainEntity":2423},"https:\u002F\u002Fschema.org","FAQPage",[2424,2429,2432,2435,2438],{"@type":2425,"name":2268,"acceptedAnswer":2426},"Question",{"@type":2427,"text":2428},"Answer","Use model_validate for a dict and model_validate_json for raw JSON bytes or a string. parse_obj and parse_raw are deprecated. Prefer model_validate_json for wire data because it parses and validates in one pass, which measures about 37% faster than calling json.loads first.",{"@type":2425,"name":2293,"acceptedAnswer":2430},{"@type":2427,"text":2431},"No. At 1M requests a month with three validations per request you spend under ten CPU-seconds total, which rounds to nothing on a $7 instance. The cost that matters is the corrupted record you bill a customer for, and that is the cost validation removes.",{"@type":2425,"name":2299,"acceptedAnswer":2433},{"@type":2427,"text":2434},"Set extra=\"ignore\" on upstream models so added fields are harmless, constrain only the fields you actually use, and alert on ValidationError counts rather than failing the whole job. A removed field then shows up as a missing error code in your metrics before a customer notices.",{"@type":2425,"name":2314,"acceptedAnswer":2436},{"@type":2427,"text":2437},"Not by default. errors() includes the raw offending input, which on inbound requests can be an email, a token, or card metadata. Call errors(include_input=False, include_url=False) and log the type and loc keys only, then keep the full payload out of any third-party log sink.",{"@type":2425,"name":2334,"acceptedAnswer":2439},{"@type":2427,"text":2440},"No. Keep the boundary model separate from your persistence model. The boundary model changes when a customer-facing contract changes, and a shared model quietly couples an API version bump to a schema migration, which is the fastest way to turn a small deprecation into a breaking release.",1784887028458]