fastapi-router-versioning 0.2.0__tar.gz → 0.2.2__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (42) hide show
  1. {fastapi_router_versioning-0.2.0 → fastapi_router_versioning-0.2.2}/PKG-INFO +23 -1
  2. {fastapi_router_versioning-0.2.0 → fastapi_router_versioning-0.2.2}/README.md +22 -0
  3. {fastapi_router_versioning-0.2.0 → fastapi_router_versioning-0.2.2}/RELEASE_NOTES.md +21 -0
  4. {fastapi_router_versioning-0.2.0 → fastapi_router_versioning-0.2.2}/examples/calver_app.py +14 -0
  5. fastapi_router_versioning-0.2.2/examples/mounted_subapps_app.py +76 -0
  6. {fastapi_router_versioning-0.2.0 → fastapi_router_versioning-0.2.2}/examples/multi_router_app.py +14 -0
  7. {fastapi_router_versioning-0.2.0 → fastapi_router_versioning-0.2.2}/examples/openapi_hook_app.py +14 -0
  8. {fastapi_router_versioning-0.2.0 → fastapi_router_versioning-0.2.2}/examples/self_hosted_docs_app.py +14 -0
  9. {fastapi_router_versioning-0.2.0 → fastapi_router_versioning-0.2.2}/examples/semver_app.py +14 -0
  10. {fastapi_router_versioning-0.2.0 → fastapi_router_versioning-0.2.2}/examples/semver_major_only_app.py +14 -0
  11. {fastapi_router_versioning-0.2.0 → fastapi_router_versioning-0.2.2}/examples/webhook_versioning_app.py +14 -0
  12. {fastapi_router_versioning-0.2.0 → fastapi_router_versioning-0.2.2}/fastapi_router_versioning/__init__.py +1 -1
  13. {fastapi_router_versioning-0.2.0 → fastapi_router_versioning-0.2.2}/fastapi_router_versioning/versioner.py +229 -199
  14. {fastapi_router_versioning-0.2.0 → fastapi_router_versioning-0.2.2}/tests/test_versioner.py +207 -2
  15. {fastapi_router_versioning-0.2.0 → fastapi_router_versioning-0.2.2}/.github/dependabot.yml +0 -0
  16. {fastapi_router_versioning-0.2.0 → fastapi_router_versioning-0.2.2}/.github/workflows/detect-conflicts.yml +0 -0
  17. {fastapi_router_versioning-0.2.0 → fastapi_router_versioning-0.2.2}/.github/workflows/pre-commit.yml +0 -0
  18. {fastapi_router_versioning-0.2.0 → fastapi_router_versioning-0.2.2}/.github/workflows/prepare-release.yml +0 -0
  19. {fastapi_router_versioning-0.2.0 → fastapi_router_versioning-0.2.2}/.github/workflows/publish.yml +0 -0
  20. {fastapi_router_versioning-0.2.0 → fastapi_router_versioning-0.2.2}/.github/workflows/test-redistribute.yml +0 -0
  21. {fastapi_router_versioning-0.2.0 → fastapi_router_versioning-0.2.2}/.github/workflows/test.yml +0 -0
  22. {fastapi_router_versioning-0.2.0 → fastapi_router_versioning-0.2.2}/.gitignore +0 -0
  23. {fastapi_router_versioning-0.2.0 → fastapi_router_versioning-0.2.2}/.pre-commit-config.yaml +0 -0
  24. {fastapi_router_versioning-0.2.0 → fastapi_router_versioning-0.2.2}/.python-version +0 -0
  25. {fastapi_router_versioning-0.2.0 → fastapi_router_versioning-0.2.2}/LICENSE +0 -0
  26. {fastapi_router_versioning-0.2.0 → fastapi_router_versioning-0.2.2}/examples/download_static_assets.py +0 -0
  27. {fastapi_router_versioning-0.2.0 → fastapi_router_versioning-0.2.2}/examples/validation_error_code_app.py +0 -0
  28. {fastapi_router_versioning-0.2.0 → fastapi_router_versioning-0.2.2}/examples/validation_error_code_custom_handler_app.py +0 -0
  29. {fastapi_router_versioning-0.2.0 → fastapi_router_versioning-0.2.2}/fastapi_router_versioning/py.typed +0 -0
  30. {fastapi_router_versioning-0.2.0 → fastapi_router_versioning-0.2.2}/pyproject.toml +0 -0
  31. {fastapi_router_versioning-0.2.0 → fastapi_router_versioning-0.2.2}/scripts/format.sh +0 -0
  32. {fastapi_router_versioning-0.2.0 → fastapi_router_versioning-0.2.2}/scripts/lint.sh +0 -0
  33. {fastapi_router_versioning-0.2.0 → fastapi_router_versioning-0.2.2}/scripts/prepare_release.py +0 -0
  34. {fastapi_router_versioning-0.2.0 → fastapi_router_versioning-0.2.2}/scripts/test-cov-html.sh +0 -0
  35. {fastapi_router_versioning-0.2.0 → fastapi_router_versioning-0.2.2}/scripts/test-cov.sh +0 -0
  36. {fastapi_router_versioning-0.2.0 → fastapi_router_versioning-0.2.2}/scripts/test.sh +0 -0
  37. {fastapi_router_versioning-0.2.0 → fastapi_router_versioning-0.2.2}/tests/__init__.py +0 -0
  38. {fastapi_router_versioning-0.2.0 → fastapi_router_versioning-0.2.2}/tests/test_calver.py +0 -0
  39. {fastapi_router_versioning-0.2.0 → fastapi_router_versioning-0.2.2}/tests/test_fastapi_integration.py +0 -0
  40. {fastapi_router_versioning-0.2.0 → fastapi_router_versioning-0.2.2}/tests/test_prepare_release.py +0 -0
  41. {fastapi_router_versioning-0.2.0 → fastapi_router_versioning-0.2.2}/tests/test_semver.py +0 -0
  42. {fastapi_router_versioning-0.2.0 → fastapi_router_versioning-0.2.2}/uv.lock +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: fastapi-router-versioning
3
- Version: 0.2.0
3
+ Version: 0.2.2
4
4
  Summary: Router-based API versioning for FastAPI, with per-version docs and a declarative route lifecycle.
5
5
  Project-URL: Homepage, https://github.com/mat81black/fastapi-router-versioning
6
6
  Project-URL: Repository, https://github.com/mat81black/fastapi-router-versioning
@@ -400,6 +400,23 @@ RouterVersioner(
400
400
  ).versionize()
401
401
  ```
402
402
 
403
+ The app's own root `/docs`, `/redoc`, and `/openapi.json` (not versioned) also reflect
404
+ `validation_error_code` — RouterVersioner patches `app.openapi()` itself, so nothing needs to
405
+ be disabled to get consistent schemas everywhere.
406
+
407
+ **If you do share one app across multiple `RouterVersioner` instances** — the only case this is
408
+ actually needed for is mixing `version_format` (e.g. SemVer for one group of routes, CalVer for
409
+ another) on the same app, since a single instance already handles multiple routers and prefixes
410
+ via `routers=[...]` (see [`multi_router_app.py`](examples/multi_router_app.py)). All instances
411
+ must use the same `validation_error_code` when `handle_validation_exceptions=True` — since
412
+ FastAPI's exception handler is app-wide, not per-router, a mismatch raises `RuntimeError` at
413
+ construction time instead of silently picking whichever was registered first. Each instance
414
+ must also use a distinct `prefix_format`/`latest_prefix`, otherwise their docs/openapi routes
415
+ would collide at the same path — this also raises `RuntimeError`.
416
+
417
+ For genuine module independence (separate `validation_error_code`, no risk of prefix collisions,
418
+ isolated docs), mount each module as its own FastAPI sub-application instead — see below.
419
+
403
420
  ### Reverse proxy / sub-app mounting
404
421
 
405
422
  When the app runs behind a reverse proxy or is mounted as a sub-application, the ASGI `root_path` is included in all per-version doc URLs automatically — no extra configuration needed:
@@ -410,6 +427,10 @@ parent.mount("/api", app) # root_path="/api" is injected at request time
410
427
  # /api/v1_0/docs correctly references /api/v1_0/openapi.json
411
428
  ```
412
429
 
430
+ Each sub-application has its own independent `RouterVersioner`, `app.state`, and OpenAPI docs —
431
+ no coordination needed between them, including different `validation_error_code` per
432
+ sub-application. See [`examples/mounted_subapps_app.py`](examples/mounted_subapps_app.py).
433
+
413
434
  ### Callback hook
414
435
 
415
436
  Run custom logic each time a versioned router is created — useful for logging or metrics:
@@ -442,6 +463,7 @@ Runnable examples are available in the [`examples/`](examples/) directory:
442
463
  | [`self_hosted_docs_app.py`](examples/self_hosted_docs_app.py) | Swagger UI and ReDoc from local static assets |
443
464
  | [`validation_error_code_app.py`](examples/validation_error_code_app.py) | Return `400` instead of `422` for validation errors (automatic handler via `RouterVersioner`) |
444
465
  | [`validation_error_code_custom_handler_app.py`](examples/validation_error_code_custom_handler_app.py) | Same, but with `handle_validation_exceptions=False` and a user-defined exception handler |
466
+ | [`mounted_subapps_app.py`](examples/mounted_subapps_app.py) | Independently versioned modules as separate FastAPI sub-applications via `app.mount()` |
445
467
 
446
468
  ---
447
469
 
@@ -371,6 +371,23 @@ RouterVersioner(
371
371
  ).versionize()
372
372
  ```
373
373
 
374
+ The app's own root `/docs`, `/redoc`, and `/openapi.json` (not versioned) also reflect
375
+ `validation_error_code` — RouterVersioner patches `app.openapi()` itself, so nothing needs to
376
+ be disabled to get consistent schemas everywhere.
377
+
378
+ **If you do share one app across multiple `RouterVersioner` instances** — the only case this is
379
+ actually needed for is mixing `version_format` (e.g. SemVer for one group of routes, CalVer for
380
+ another) on the same app, since a single instance already handles multiple routers and prefixes
381
+ via `routers=[...]` (see [`multi_router_app.py`](examples/multi_router_app.py)). All instances
382
+ must use the same `validation_error_code` when `handle_validation_exceptions=True` — since
383
+ FastAPI's exception handler is app-wide, not per-router, a mismatch raises `RuntimeError` at
384
+ construction time instead of silently picking whichever was registered first. Each instance
385
+ must also use a distinct `prefix_format`/`latest_prefix`, otherwise their docs/openapi routes
386
+ would collide at the same path — this also raises `RuntimeError`.
387
+
388
+ For genuine module independence (separate `validation_error_code`, no risk of prefix collisions,
389
+ isolated docs), mount each module as its own FastAPI sub-application instead — see below.
390
+
374
391
  ### Reverse proxy / sub-app mounting
375
392
 
376
393
  When the app runs behind a reverse proxy or is mounted as a sub-application, the ASGI `root_path` is included in all per-version doc URLs automatically — no extra configuration needed:
@@ -381,6 +398,10 @@ parent.mount("/api", app) # root_path="/api" is injected at request time
381
398
  # /api/v1_0/docs correctly references /api/v1_0/openapi.json
382
399
  ```
383
400
 
401
+ Each sub-application has its own independent `RouterVersioner`, `app.state`, and OpenAPI docs —
402
+ no coordination needed between them, including different `validation_error_code` per
403
+ sub-application. See [`examples/mounted_subapps_app.py`](examples/mounted_subapps_app.py).
404
+
384
405
  ### Callback hook
385
406
 
386
407
  Run custom logic each time a versioned router is created — useful for logging or metrics:
@@ -413,6 +434,7 @@ Runnable examples are available in the [`examples/`](examples/) directory:
413
434
  | [`self_hosted_docs_app.py`](examples/self_hosted_docs_app.py) | Swagger UI and ReDoc from local static assets |
414
435
  | [`validation_error_code_app.py`](examples/validation_error_code_app.py) | Return `400` instead of `422` for validation errors (automatic handler via `RouterVersioner`) |
415
436
  | [`validation_error_code_custom_handler_app.py`](examples/validation_error_code_custom_handler_app.py) | Same, but with `handle_validation_exceptions=False` and a user-defined exception handler |
437
+ | [`mounted_subapps_app.py`](examples/mounted_subapps_app.py) | Independently versioned modules as separate FastAPI sub-applications via `app.mount()` |
416
438
 
417
439
  ---
418
440
 
@@ -2,6 +2,27 @@
2
2
 
3
3
  ## Latest Changes
4
4
 
5
+ ## 0.2.2 (2026-07-01)
6
+
7
+ ### Fixes
8
+
9
+ * 🐛 Fix root OpenAPI schema not reflecting validation_error_code. PR [#21](https://github.com/mat81black/fastapi-router-versioning/pull/21) by [@mat81black](https://github.com/mat81black).
10
+ * 🐛 Fix silent conflicts from misconfigured RouterVersioner instances sharing an app. PR [#20](https://github.com/mat81black/fastapi-router-versioning/pull/20) by [@mat81black](https://github.com/mat81black).
11
+
12
+ ### Internal
13
+
14
+ * 👷 Add a regression test for `RouterVersioner` under a real `app.mount()` sub-application, and `examples/mounted_subapps_app.py` demonstrating the same scenario. PR [#22](https://github.com/mat81black/fastapi-router-versioning/pull/22) by [@mat81black](https://github.com/mat81black).
15
+
16
+ ### Refactors
17
+
18
+ * ♻️ Reorder RouterVersioner methods to match their call graph. PR [#23](https://github.com/mat81black/fastapi-router-versioning/pull/23) by [@mat81black](https://github.com/mat81black).
19
+
20
+ ## 0.2.1 (2026-06-26)
21
+
22
+ ### Fixes
23
+
24
+ * 🐛 Fix `api_version` signature to enforce `deprecate_in` and `remove_in` as keyword-only arguments, aligning the implementation with the documented signature. PR [#18](https://github.com/mat81black/fastapi-router-versioning/pull/18) by [@mat81black](https://github.com/mat81black).
25
+
5
26
  ## 0.2.0 (2026-06-26)
6
27
 
7
28
  ### Features
@@ -1,4 +1,5 @@
1
1
  from fastapi import APIRouter, FastAPI
2
+ from pydantic import BaseModel
2
3
 
3
4
  from fastapi_router_versioning import RouterVersioner, VersionFormat, api_version
4
5
 
@@ -10,6 +11,11 @@ app = FastAPI(
10
11
  router = APIRouter()
11
12
 
12
13
 
14
+ class CreateItemRequest(BaseModel):
15
+ name: str
16
+ quantity: int
17
+
18
+
13
19
  # 1. Introduced in the January release, persists across all versions
14
20
  @router.get("/persistent")
15
21
  @api_version("2025-01-01")
@@ -41,6 +47,14 @@ def future_route() -> dict[str, str]:
41
47
  return {"status": "active", "message": "Welcome to the December release!"}
42
48
 
43
49
 
50
+ # POST /items with an invalid "quantity" (e.g. "not-a-number") returns FastAPI's
51
+ # default 422 validation error.
52
+ @router.post("/items")
53
+ @api_version("2025-01-01")
54
+ def create_item(body: CreateItemRequest) -> dict[str, str]:
55
+ return {"name": body.name, "quantity": str(body.quantity)}
56
+
57
+
44
58
  versioner = RouterVersioner(
45
59
  app=app,
46
60
  routers=router,
@@ -0,0 +1,76 @@
1
+ """
2
+ Sub-applications example.
3
+
4
+ Shows how to mount independently versioned modules as separate FastAPI
5
+ sub-applications via app.mount() — each with its own RouterVersioner, docs,
6
+ and OpenAPI schema. RouterVersioner already threads the ASGI root_path through
7
+ docs/openapi URLs automatically, so no extra configuration is needed.
8
+ """
9
+
10
+ from fastapi import APIRouter, FastAPI
11
+ from pydantic import BaseModel
12
+
13
+ from fastapi_router_versioning import RouterVersioner, VersionFormat, api_version
14
+
15
+
16
+ class CreateItemRequest(BaseModel):
17
+ name: str
18
+ quantity: int
19
+
20
+
21
+ main_app = FastAPI(title="Main App")
22
+
23
+ # ── Admin sub-app: SemVer, validation errors return 400 ──────────────────────
24
+
25
+ admin_app = FastAPI(title="Admin API")
26
+ admin_router = APIRouter()
27
+
28
+
29
+ @admin_router.get("/users")
30
+ @api_version((1, 0))
31
+ def list_admin_users() -> dict[str, list[str]]:
32
+ return {"users": []}
33
+
34
+
35
+ @admin_router.post("/items")
36
+ @api_version((1, 0))
37
+ def create_admin_item(body: CreateItemRequest) -> dict[str, str]:
38
+ return {"name": body.name, "quantity": str(body.quantity)}
39
+
40
+
41
+ RouterVersioner(
42
+ app=admin_app, routers=admin_router, version_format=VersionFormat.SEMVER, validation_error_code=400
43
+ ).versionize()
44
+
45
+
46
+ # ── Orders sub-app: CalVer ────────────────────────────────────────────────────
47
+
48
+ orders_app = FastAPI(title="Orders API")
49
+ orders_router = APIRouter()
50
+
51
+
52
+ @orders_router.get("/")
53
+ @api_version("2025-01-01")
54
+ def list_orders() -> dict[str, list[str]]:
55
+ return {"orders": []}
56
+
57
+
58
+ @orders_router.post("/items")
59
+ @api_version("2025-01-01")
60
+ def create_order_item(body: CreateItemRequest) -> dict[str, str]:
61
+ return {"name": body.name, "quantity": str(body.quantity)}
62
+
63
+
64
+ RouterVersioner(app=orders_app, routers=orders_router, version_format=VersionFormat.CALVER).versionize()
65
+
66
+
67
+ # ── Mount both sub-apps on the main app ───────────────────────────────────────
68
+
69
+ main_app.mount("/admin", admin_app)
70
+ main_app.mount("/orders", orders_app)
71
+
72
+
73
+ if __name__ == "__main__":
74
+ import uvicorn
75
+
76
+ uvicorn.run(main_app, host="127.0.0.1", port=8009)
@@ -8,6 +8,7 @@ versioned together under a single prefix tree.
8
8
  """
9
9
 
10
10
  from fastapi import APIRouter, FastAPI
11
+ from pydantic import BaseModel
11
12
 
12
13
  from fastapi_router_versioning import RouterVersioner, VersionFormat, api_version
13
14
 
@@ -20,6 +21,11 @@ users_router = APIRouter(prefix="/users", tags=["Users"])
20
21
  products_router = APIRouter(prefix="/products", tags=["Products"])
21
22
 
22
23
 
24
+ class CreateProductRequest(BaseModel):
25
+ name: str
26
+ quantity: int
27
+
28
+
23
29
  @users_router.get("/")
24
30
  @api_version((1, 0))
25
31
  def list_users() -> dict[str, list[str]]:
@@ -44,6 +50,14 @@ def search_products() -> dict[str, list[str]]:
44
50
  return {"results": []}
45
51
 
46
52
 
53
+ # POST /products with an invalid "quantity" (e.g. "not-a-number") returns FastAPI's
54
+ # default 422 validation error.
55
+ @products_router.post("/")
56
+ @api_version((1, 0))
57
+ def create_product(body: CreateProductRequest) -> dict[str, str]:
58
+ return {"name": body.name, "quantity": str(body.quantity)}
59
+
60
+
47
61
  versioner = RouterVersioner(
48
62
  app=app,
49
63
  routers=[users_router, products_router], # list of routers versioned together
@@ -1,6 +1,7 @@
1
1
  from typing import Any
2
2
 
3
3
  from fastapi import APIRouter, FastAPI
4
+ from pydantic import BaseModel
4
5
 
5
6
  from fastapi_router_versioning import RouterVersioner, VersionFormat, VersionT, api_version
6
7
 
@@ -12,6 +13,11 @@ app = FastAPI(
12
13
  router = APIRouter()
13
14
 
14
15
 
16
+ class CreateItemRequest(BaseModel):
17
+ name: str
18
+ quantity: int
19
+
20
+
15
21
  @router.get("/items")
16
22
  @api_version((1, 0))
17
23
  def get_items_v1() -> dict[str, object]:
@@ -30,6 +36,14 @@ def get_users() -> dict[str, object]:
30
36
  return {"users": ["alice", "bob"]}
31
37
 
32
38
 
39
+ # POST /items with an invalid "quantity" (e.g. "not-a-number") returns FastAPI's
40
+ # default 422 validation error.
41
+ @router.post("/items")
42
+ @api_version((1, 0))
43
+ def create_item(body: CreateItemRequest) -> dict[str, str]:
44
+ return {"name": body.name, "quantity": str(body.quantity)}
45
+
46
+
33
47
  def my_openapi_hook(schema: dict[str, Any], version: VersionT) -> dict[str, Any]:
34
48
  # Applied to every version: add a company logo to the docs
35
49
  schema["info"]["x-logo"] = {"url": "https://fastapi.tiangolo.com/img/logo-margin/logo-teal.png"}
@@ -18,6 +18,7 @@ from pathlib import Path
18
18
 
19
19
  from fastapi import APIRouter, FastAPI
20
20
  from fastapi.staticfiles import StaticFiles
21
+ from pydantic import BaseModel
21
22
 
22
23
  from fastapi_router_versioning import RouterVersioner, VersionFormat, api_version
23
24
 
@@ -32,12 +33,25 @@ app.mount("/static", StaticFiles(directory=STATIC_DIR), name="static")
32
33
  router = APIRouter()
33
34
 
34
35
 
36
+ class CreateItemRequest(BaseModel):
37
+ name: str
38
+ quantity: int
39
+
40
+
35
41
  @router.get("/items")
36
42
  @api_version((1, 0))
37
43
  def list_items() -> dict[str, list[str]]:
38
44
  return {"items": []}
39
45
 
40
46
 
47
+ # POST /items with an invalid "quantity" (e.g. "not-a-number") returns FastAPI's
48
+ # default 422 validation error.
49
+ @router.post("/items")
50
+ @api_version((1, 0))
51
+ def create_item(body: CreateItemRequest) -> dict[str, str]:
52
+ return {"name": body.name, "quantity": str(body.quantity)}
53
+
54
+
41
55
  versioner = RouterVersioner(
42
56
  app=app,
43
57
  routers=router,
@@ -1,4 +1,5 @@
1
1
  from fastapi import APIRouter, FastAPI
2
+ from pydantic import BaseModel
2
3
 
3
4
  from fastapi_router_versioning import RouterVersioner, VersionFormat, api_version
4
5
 
@@ -10,6 +11,11 @@ app = FastAPI(
10
11
  router = APIRouter()
11
12
 
12
13
 
14
+ class CreateItemRequest(BaseModel):
15
+ name: str
16
+ quantity: int
17
+
18
+
13
19
  # 1. Introduced in v1.0, persists across all versions
14
20
  @router.get("/persistent")
15
21
  @api_version((1, 0))
@@ -41,6 +47,14 @@ def future_route() -> dict[str, str]:
41
47
  return {"status": "active", "message": "Welcome to v3.0!"}
42
48
 
43
49
 
50
+ # POST /items with an invalid "quantity" (e.g. "not-a-number") returns FastAPI's
51
+ # default 422 validation error.
52
+ @router.post("/items")
53
+ @api_version((1, 0))
54
+ def create_item(body: CreateItemRequest) -> dict[str, str]:
55
+ return {"name": body.name, "quantity": str(body.quantity)}
56
+
57
+
44
58
  versioner = RouterVersioner(
45
59
  app=app,
46
60
  routers=router,
@@ -1,4 +1,5 @@
1
1
  from fastapi import APIRouter, FastAPI
2
+ from pydantic import BaseModel
2
3
 
3
4
  from fastapi_router_versioning import RouterVersioner, VersionFormat, api_version
4
5
 
@@ -10,6 +11,11 @@ app = FastAPI(
10
11
  router = APIRouter()
11
12
 
12
13
 
14
+ class CreateItemRequest(BaseModel):
15
+ name: str
16
+ quantity: int
17
+
18
+
13
19
  # 1. Introduced in v1, persists across all versions
14
20
  @router.get("/persistent")
15
21
  @api_version((1, 0))
@@ -41,6 +47,14 @@ def future_route() -> dict[str, str]:
41
47
  return {"status": "active", "message": "Welcome to v3!"}
42
48
 
43
49
 
50
+ # POST /items with an invalid "quantity" (e.g. "not-a-number") returns FastAPI's
51
+ # default 422 validation error.
52
+ @router.post("/items")
53
+ @api_version((1, 0))
54
+ def create_item(body: CreateItemRequest) -> dict[str, str]:
55
+ return {"name": body.name, "quantity": str(body.quantity)}
56
+
57
+
44
58
  versioner = RouterVersioner(
45
59
  app=app,
46
60
  routers=router,
@@ -1,4 +1,5 @@
1
1
  from fastapi import APIRouter, FastAPI
2
+ from pydantic import BaseModel
2
3
 
3
4
  from fastapi_router_versioning import RouterVersioner, VersionFormat, api_version
4
5
 
@@ -12,6 +13,11 @@ app = FastAPI(
12
13
  router = APIRouter()
13
14
 
14
15
 
16
+ class CreateItemRequest(BaseModel):
17
+ name: str
18
+ quantity: int
19
+
20
+
15
21
  @router.post("/orders")
16
22
  @api_version((1, 0))
17
23
  def create_order_v1() -> dict[str, str]:
@@ -30,6 +36,14 @@ def get_items() -> dict[str, object]:
30
36
  return {"items": ["a", "b"]}
31
37
 
32
38
 
39
+ # POST /items with an invalid "quantity" (e.g. "not-a-number") returns FastAPI's
40
+ # default 422 validation error.
41
+ @router.post("/items")
42
+ @api_version((1, 0))
43
+ def create_item(body: CreateItemRequest) -> dict[str, str]:
44
+ return {"name": body.name, "quantity": str(body.quantity)}
45
+
46
+
33
47
  # --- Webhook definitions ---
34
48
  # Webhooks document outbound calls your API makes to subscriber URLs.
35
49
  # Annotate them with @api_version just like regular routes.
@@ -1,5 +1,5 @@
1
1
  from .versioner import RouterVersioner, VersionFormat, VersionT, api_version
2
2
 
3
- __version__ = "0.2.0"
3
+ __version__ = "0.2.2"
4
4
 
5
5
  __all__ = ["RouterVersioner", "api_version", "VersionFormat", "VersionT"]