{"id":"9ef8f6ae1df13759","repo":"tiangolo/fastapi","slug":"invalid-id-format-it-must-start-with-isbn-or","errorCode":null,"errorMessage":"Invalid ID format, it must start with \"isbn-\" or \"imdb-\"","messagePattern":"Invalid ID format, it must start with \"isbn-\" or \"imdb-\"","errorType":"validation","errorClass":"ValueError","httpStatus":422,"severity":"error","filePath":"docs_src/query_params_str_validations/tutorial015_an_py310.py","lineNumber":18,"sourceCode":"import random\nfrom typing import Annotated\n\nfrom fastapi import FastAPI\nfrom pydantic import AfterValidator\n\napp = FastAPI()\n\ndata = {\n    \"isbn-9781529046137\": \"The Hitchhiker's Guide to the Galaxy\",\n    \"imdb-tt0371724\": \"The Hitchhiker's Guide to the Galaxy\",\n    \"isbn-9781439512982\": \"Isaac Asimov: The Complete Stories, Vol. 2\",\n}\n\n\ndef check_valid_id(id: str):\n    if not id.startswith((\"isbn-\", \"imdb-\")):\n        raise ValueError('Invalid ID format, it must start with \"isbn-\" or \"imdb-\"')\n    return id\n\n\n@app.get(\"/items/\")\nasync def read_items(\n    id: Annotated[str | None, AfterValidator(check_valid_id)] = None,\n):\n    if id:\n        item = data.get(id)\n    else:\n        id, item = random.choice(list(data.items()))\n    return {\"id\": id, \"name\": item}\n","sourceCodeStart":1,"sourceCodeEnd":31,"githubUrl":"https://github.com/tiangolo/fastapi/blob/42a41db11f6882807ac3c057b942178d53b97438/docs_src/query_params_str_validations/tutorial015_an_py310.py#L1-L31","documentation":"This `ValueError` is raised by `check_valid_id`, used as a Pydantic `AfterValidator` on the `id` query parameter of `GET /items/`. Because it is raised inside a validator, Pydantic converts it into a 422 response whose `detail` echoes this message. It enforces that any explicitly supplied `id` must be prefixed with `isbn-` or `imdb-`; omitting `id` is allowed (the route then picks one at random).","triggerScenarios":"`GET /items/?id=9781529046137` (no prefix), `?id=foo-123`, or `?id=isbn` (prefix only). Each maps to a key in the in-memory `data` dict that uses the `isbn-`/`imdb-` prefix scheme.","commonSituations":"Front-end passing a raw barcode without the source prefix; IDs copied from another system; changing the accepted prefix tuple without updating callers; trailing whitespace breaking `startswith`.","solutions":["Prefix the id correctly, e.g. `?id=isbn-9781529046137`.","Omit `id` to let the route return a random item.","If new prefixes are legitimate, extend `startswith((\"isbn-\", \"imdb-\"))` and redeploy."],"exampleFix":"# before\nGET /items/?id=9781529046137\n# after\nGET /items/?id=isbn-9781529046137","handlingStrategy":"validation","validationCode":"ALLOWED = (\"isbn-\", \"imdb-\")\ndef valid_id(id_: str | None) -> bool:\n    return id_ is None or id_.strip().startswith(ALLOWED)\n# only include params={\"id\": id_} when valid_id(id_) is True","typeGuard":"def is_valid_id(id_: object) -> bool:\n    return isinstance(id_, str) and id_.startswith((\"isbn-\", \"imdb-\"))","tryCatchPattern":"r = client.get(\"/items/\", params={\"id\": id_} if id_ else {})\nif r.status_code == 422:\n    raise ValueError(f\"bad id format: {r.json()['detail']}\")","preventionTips":["Centralize the allowed-prefix list in a constant shared by client and server.","Strip whitespace before appending the id to the query string.","Treat a missing prefix as a client bug, not a retryable condition."],"tags":["validation","query-params","aftervalidator","pydantic","fastapi"],"analyzedSha":"42a41db11f6882807ac3c057b942178d53b97438","analyzedAt":"2026-08-04T19:23:32.007Z","schemaVersion":2}