fastapi-router-versioning 0.2.7__tar.gz → 0.3.0__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 (50) hide show
  1. {fastapi_router_versioning-0.2.7 → fastapi_router_versioning-0.3.0}/.github/workflows/bump-pre-commit-hooks.yml +3 -3
  2. {fastapi_router_versioning-0.2.7 → fastapi_router_versioning-0.3.0}/.github/workflows/create-draft-release.yml +3 -3
  3. {fastapi_router_versioning-0.2.7 → fastapi_router_versioning-0.3.0}/.github/workflows/labeler.yml +1 -1
  4. {fastapi_router_versioning-0.2.7 → fastapi_router_versioning-0.3.0}/.github/workflows/latest-changes.yml +1 -1
  5. {fastapi_router_versioning-0.2.7 → fastapi_router_versioning-0.3.0}/.github/workflows/pre-commit.yml +3 -3
  6. {fastapi_router_versioning-0.2.7 → fastapi_router_versioning-0.3.0}/.github/workflows/prepare-release.yml +3 -3
  7. {fastapi_router_versioning-0.2.7 → fastapi_router_versioning-0.3.0}/.github/workflows/publish.yml +3 -3
  8. {fastapi_router_versioning-0.2.7 → fastapi_router_versioning-0.3.0}/.github/workflows/test-redistribute.yml +3 -3
  9. {fastapi_router_versioning-0.2.7 → fastapi_router_versioning-0.3.0}/.github/workflows/test.yml +6 -6
  10. {fastapi_router_versioning-0.2.7 → fastapi_router_versioning-0.3.0}/.github/workflows/zizmor.yml +2 -2
  11. {fastapi_router_versioning-0.2.7 → fastapi_router_versioning-0.3.0}/PKG-INFO +44 -58
  12. {fastapi_router_versioning-0.2.7 → fastapi_router_versioning-0.3.0}/README.md +43 -57
  13. {fastapi_router_versioning-0.2.7 → fastapi_router_versioning-0.3.0}/RELEASE_NOTES.md +12 -0
  14. {fastapi_router_versioning-0.2.7 → fastapi_router_versioning-0.3.0}/examples/mounted_subapps_app.py +2 -4
  15. fastapi_router_versioning-0.3.0/examples/validation_override_integration_app.py +56 -0
  16. {fastapi_router_versioning-0.2.7 → fastapi_router_versioning-0.3.0}/fastapi_router_versioning/__init__.py +1 -1
  17. {fastapi_router_versioning-0.2.7 → fastapi_router_versioning-0.3.0}/fastapi_router_versioning/versioner.py +0 -99
  18. {fastapi_router_versioning-0.2.7 → fastapi_router_versioning-0.3.0}/pyproject.toml +1 -0
  19. {fastapi_router_versioning-0.2.7 → fastapi_router_versioning-0.3.0}/tests/test_versioner.py +7 -330
  20. {fastapi_router_versioning-0.2.7 → fastapi_router_versioning-0.3.0}/uv.lock +131 -117
  21. fastapi_router_versioning-0.2.7/examples/validation_error_code_app.py +0 -57
  22. fastapi_router_versioning-0.2.7/examples/validation_error_code_custom_handler_app.py +0 -70
  23. {fastapi_router_versioning-0.2.7 → fastapi_router_versioning-0.3.0}/.github/dependabot.yml +0 -0
  24. {fastapi_router_versioning-0.2.7 → fastapi_router_versioning-0.3.0}/.github/labeler.yml +0 -0
  25. {fastapi_router_versioning-0.2.7 → fastapi_router_versioning-0.3.0}/.github/workflows/detect-conflicts.yml +0 -0
  26. {fastapi_router_versioning-0.2.7 → fastapi_router_versioning-0.3.0}/.github/workflows/guard-dependencies.yml +0 -0
  27. {fastapi_router_versioning-0.2.7 → fastapi_router_versioning-0.3.0}/.gitignore +0 -0
  28. {fastapi_router_versioning-0.2.7 → fastapi_router_versioning-0.3.0}/.pre-commit-config.yaml +0 -0
  29. {fastapi_router_versioning-0.2.7 → fastapi_router_versioning-0.3.0}/.python-version +0 -0
  30. {fastapi_router_versioning-0.2.7 → fastapi_router_versioning-0.3.0}/LICENSE +0 -0
  31. {fastapi_router_versioning-0.2.7 → fastapi_router_versioning-0.3.0}/examples/calver_app.py +0 -0
  32. {fastapi_router_versioning-0.2.7 → fastapi_router_versioning-0.3.0}/examples/download_static_assets.py +0 -0
  33. {fastapi_router_versioning-0.2.7 → fastapi_router_versioning-0.3.0}/examples/multi_router_app.py +0 -0
  34. {fastapi_router_versioning-0.2.7 → fastapi_router_versioning-0.3.0}/examples/openapi_hook_app.py +0 -0
  35. {fastapi_router_versioning-0.2.7 → fastapi_router_versioning-0.3.0}/examples/self_hosted_docs_app.py +0 -0
  36. {fastapi_router_versioning-0.2.7 → fastapi_router_versioning-0.3.0}/examples/semver_app.py +0 -0
  37. {fastapi_router_versioning-0.2.7 → fastapi_router_versioning-0.3.0}/examples/semver_major_only_app.py +0 -0
  38. {fastapi_router_versioning-0.2.7 → fastapi_router_versioning-0.3.0}/examples/webhook_versioning_app.py +0 -0
  39. {fastapi_router_versioning-0.2.7 → fastapi_router_versioning-0.3.0}/fastapi_router_versioning/py.typed +0 -0
  40. {fastapi_router_versioning-0.2.7 → fastapi_router_versioning-0.3.0}/scripts/format.sh +0 -0
  41. {fastapi_router_versioning-0.2.7 → fastapi_router_versioning-0.3.0}/scripts/lint.sh +0 -0
  42. {fastapi_router_versioning-0.2.7 → fastapi_router_versioning-0.3.0}/scripts/prepare_release.py +0 -0
  43. {fastapi_router_versioning-0.2.7 → fastapi_router_versioning-0.3.0}/scripts/test-cov-html.sh +0 -0
  44. {fastapi_router_versioning-0.2.7 → fastapi_router_versioning-0.3.0}/scripts/test-cov.sh +0 -0
  45. {fastapi_router_versioning-0.2.7 → fastapi_router_versioning-0.3.0}/scripts/test.sh +0 -0
  46. {fastapi_router_versioning-0.2.7 → fastapi_router_versioning-0.3.0}/tests/__init__.py +0 -0
  47. {fastapi_router_versioning-0.2.7 → fastapi_router_versioning-0.3.0}/tests/test_calver.py +0 -0
  48. {fastapi_router_versioning-0.2.7 → fastapi_router_versioning-0.3.0}/tests/test_fastapi_integration.py +0 -0
  49. {fastapi_router_versioning-0.2.7 → fastapi_router_versioning-0.3.0}/tests/test_prepare_release.py +0 -0
  50. {fastapi_router_versioning-0.2.7 → fastapi_router_versioning-0.3.0}/tests/test_semver.py +0 -0
@@ -17,16 +17,16 @@ jobs:
17
17
  env:
18
18
  GITHUB_CONTEXT: ${{ toJson(github) }}
19
19
  run: echo "$GITHUB_CONTEXT"
20
- - uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0
20
+ - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
21
21
  with:
22
22
  token: ${{ secrets.FASTAPI_ROUTER_VERSIONING_LATEST_CHANGES }}
23
23
  persist-credentials: true
24
24
  - name: Set up Python
25
- uses: actions/setup-python@ece7cb06caefa5fff74198d8649806c4678c61a1 # v6.3.0
25
+ uses: actions/setup-python@5fda3b95a4ea91299a34e894583c3862153e4b97 # v7.0.0
26
26
  with:
27
27
  python-version-file: ".python-version"
28
28
  - name: Setup uv
29
- uses: astral-sh/setup-uv@11f9893b081a58869d3b5fccaea48c9e9e46f990 # v8.3.2
29
+ uses: astral-sh/setup-uv@c771a70e6277c0a99b617c7a806ffedaca235ff9 # v9.0.0
30
30
  with:
31
31
  # Before upgrading uv version, make sure astral-sh/setup-uv knows its checksum.
32
32
  # See: https://github.com/astral-sh/setup-uv/issues/851#issuecomment-4282017837
@@ -22,16 +22,16 @@ jobs:
22
22
  env:
23
23
  GITHUB_CONTEXT: ${{ toJson(github) }}
24
24
  run: echo "$GITHUB_CONTEXT"
25
- - uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0
25
+ - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
26
26
  with:
27
27
  ref: ${{ github.event.repository.default_branch }}
28
28
  persist-credentials: true
29
29
  - name: Set up Python
30
- uses: actions/setup-python@ece7cb06caefa5fff74198d8649806c4678c61a1 # v6.3.0
30
+ uses: actions/setup-python@5fda3b95a4ea91299a34e894583c3862153e4b97 # v7.0.0
31
31
  with:
32
32
  python-version-file: ".python-version"
33
33
  - name: Install uv
34
- uses: astral-sh/setup-uv@11f9893b081a58869d3b5fccaea48c9e9e46f990 # v8.3.2
34
+ uses: astral-sh/setup-uv@c771a70e6277c0a99b617c7a806ffedaca235ff9 # v9.0.0
35
35
  with:
36
36
  # Before upgrading uv version, make sure astral-sh/setup-uv knows its checksum.
37
37
  # See: https://github.com/astral-sh/setup-uv/issues/851#issuecomment-4282017837
@@ -19,7 +19,7 @@ jobs:
19
19
  runs-on: ubuntu-latest
20
20
  timeout-minutes: 5
21
21
  steps:
22
- - uses: actions/labeler@b8dd2d9be0f68b860e7dae5dae7d772984eacd6d # v6.2.0
22
+ - uses: actions/labeler@bf12e9b00b37c5c0ca2b87b79b2daf7891dbda13 # v7.0.0
23
23
  if: ${{ github.event.action != 'labeled' && github.event.action != 'unlabeled' }}
24
24
  - run: echo "Done adding labels"
25
25
  # Run this after labeler applied labels
@@ -28,7 +28,7 @@ jobs:
28
28
  env:
29
29
  GITHUB_CONTEXT: ${{ toJson(github) }}
30
30
  run: echo "$GITHUB_CONTEXT"
31
- - uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0
31
+ - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
32
32
  with:
33
33
  ref: ${{ github.event.repository.default_branch }}
34
34
  # To allow latest-changes to commit to the main branch
@@ -18,19 +18,19 @@ jobs:
18
18
  run: echo "$GITHUB_CONTEXT"
19
19
 
20
20
  - name: Checkout code
21
- uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0
21
+ uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
22
22
  with:
23
23
  persist-credentials: false
24
24
  # Full history required for --from-ref/--to-ref diff in prek
25
25
  fetch-depth: 0
26
26
 
27
27
  - name: Set up Python
28
- uses: actions/setup-python@ece7cb06caefa5fff74198d8649806c4678c61a1 # v6.3.0
28
+ uses: actions/setup-python@5fda3b95a4ea91299a34e894583c3862153e4b97 # v7.0.0
29
29
  with:
30
30
  python-version-file: ".python-version"
31
31
 
32
32
  - name: Setup uv
33
- uses: astral-sh/setup-uv@11f9893b081a58869d3b5fccaea48c9e9e46f990 # v8.3.2
33
+ uses: astral-sh/setup-uv@c771a70e6277c0a99b617c7a806ffedaca235ff9 # v9.0.0
34
34
  with:
35
35
  # Before upgrading uv version, make sure astral-sh/setup-uv knows its checksum.
36
36
  # See: https://github.com/astral-sh/setup-uv/issues/851#issuecomment-4282017837
@@ -34,16 +34,16 @@ jobs:
34
34
  env:
35
35
  GITHUB_CONTEXT: ${{ toJson(github) }}
36
36
  run: echo "$GITHUB_CONTEXT"
37
- - uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0
37
+ - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
38
38
  with:
39
39
  token: ${{ secrets.FASTAPI_ROUTER_VERSIONING_LATEST_CHANGES }} # zizmor: ignore[secrets-outside-env]
40
40
  persist-credentials: true
41
41
  - name: Set up Python
42
- uses: actions/setup-python@ece7cb06caefa5fff74198d8649806c4678c61a1 # v6.3.0
42
+ uses: actions/setup-python@5fda3b95a4ea91299a34e894583c3862153e4b97 # v7.0.0
43
43
  with:
44
44
  python-version-file: ".python-version"
45
45
  - name: Install uv
46
- uses: astral-sh/setup-uv@11f9893b081a58869d3b5fccaea48c9e9e46f990 # v8.3.2
46
+ uses: astral-sh/setup-uv@c771a70e6277c0a99b617c7a806ffedaca235ff9 # v9.0.0
47
47
  with:
48
48
  # Before upgrading uv version, make sure astral-sh/setup-uv knows its checksum.
49
49
  # See: https://github.com/astral-sh/setup-uv/issues/851#issuecomment-4282017837
@@ -20,15 +20,15 @@ jobs:
20
20
  env:
21
21
  GITHUB_CONTEXT: ${{ toJson(github) }}
22
22
  run: echo "$GITHUB_CONTEXT"
23
- - uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0
23
+ - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
24
24
  with:
25
25
  persist-credentials: false
26
26
  - name: Set up Python
27
- uses: actions/setup-python@ece7cb06caefa5fff74198d8649806c4678c61a1 # v6.3.0
27
+ uses: actions/setup-python@5fda3b95a4ea91299a34e894583c3862153e4b97 # v7.0.0
28
28
  with:
29
29
  python-version-file: ".python-version"
30
30
  - name: Install uv
31
- uses: astral-sh/setup-uv@11f9893b081a58869d3b5fccaea48c9e9e46f990 # v8.3.2
31
+ uses: astral-sh/setup-uv@c771a70e6277c0a99b617c7a806ffedaca235ff9 # v9.0.0
32
32
  with:
33
33
  # Before upgrading uv version, make sure astral-sh/setup-uv knows its checksum.
34
34
  # See: https://github.com/astral-sh/setup-uv/issues/851#issuecomment-4282017837
@@ -18,7 +18,7 @@ jobs:
18
18
  outputs:
19
19
  src: ${{ steps.filter.outputs.src }}
20
20
  steps:
21
- - uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0
21
+ - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
22
22
  with:
23
23
  persist-credentials: false
24
24
  - uses: dorny/paths-filter@7b450fff21473bca461d4b92ce414b9d0420d706 # v4.0.2
@@ -46,11 +46,11 @@ jobs:
46
46
  env:
47
47
  GITHUB_CONTEXT: ${{ toJson(github) }}
48
48
  run: echo "$GITHUB_CONTEXT"
49
- - uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0
49
+ - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
50
50
  with:
51
51
  persist-credentials: false
52
52
  - name: Set up Python
53
- uses: actions/setup-python@ece7cb06caefa5fff74198d8649806c4678c61a1 # v6.3.0
53
+ uses: actions/setup-python@5fda3b95a4ea91299a34e894583c3862153e4b97 # v7.0.0
54
54
  with:
55
55
  python-version-file: ".python-version"
56
56
  - name: Install build dependencies
@@ -26,7 +26,7 @@ jobs:
26
26
  outputs:
27
27
  src: ${{ steps.filter.outputs.src }}
28
28
  steps:
29
- - uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0
29
+ - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
30
30
  with:
31
31
  persist-credentials: false
32
32
  # For pull requests it's not necessary to checkout the code but for the main branch it is
@@ -72,17 +72,17 @@ jobs:
72
72
  GITHUB_CONTEXT: ${{ toJson(github) }}
73
73
  run: echo "$GITHUB_CONTEXT"
74
74
 
75
- - uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0
75
+ - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
76
76
  with:
77
77
  persist-credentials: false
78
78
 
79
79
  - name: Set up Python
80
- uses: actions/setup-python@ece7cb06caefa5fff74198d8649806c4678c61a1 # v6.3.0
80
+ uses: actions/setup-python@5fda3b95a4ea91299a34e894583c3862153e4b97 # v7.0.0
81
81
  with:
82
82
  python-version: ${{ matrix.python-version }}
83
83
 
84
84
  - name: Setup uv
85
- uses: astral-sh/setup-uv@11f9893b081a58869d3b5fccaea48c9e9e46f990 # v8.3.2
85
+ uses: astral-sh/setup-uv@c771a70e6277c0a99b617c7a806ffedaca235ff9 # v9.0.0
86
86
  with:
87
87
  # Before upgrading uv version, make sure astral-sh/setup-uv knows its checksum.
88
88
  # See: https://github.com/astral-sh/setup-uv/issues/851#issuecomment-4282017837
@@ -130,12 +130,12 @@ jobs:
130
130
  GITHUB_CONTEXT: ${{ toJson(github) }}
131
131
  run: echo "$GITHUB_CONTEXT"
132
132
 
133
- - uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0
133
+ - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
134
134
  with:
135
135
  persist-credentials: false
136
136
 
137
137
  - name: Setup uv
138
- uses: astral-sh/setup-uv@11f9893b081a58869d3b5fccaea48c9e9e46f990 # v8.3.2
138
+ uses: astral-sh/setup-uv@c771a70e6277c0a99b617c7a806ffedaca235ff9 # v9.0.0
139
139
  with:
140
140
  # Before upgrading uv version, make sure astral-sh/setup-uv knows its checksum.
141
141
  # See: https://github.com/astral-sh/setup-uv/issues/851#issuecomment-4282017837
@@ -18,8 +18,8 @@ jobs:
18
18
  security-events: write # Required for upload-sarif (used by zizmor-action) to upload SARIF files.
19
19
  steps:
20
20
  - name: Checkout repository
21
- uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0
21
+ uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
22
22
  with:
23
23
  persist-credentials: false
24
24
  - name: Run zizmor
25
- uses: zizmorcore/zizmor-action@192e21d79ab29983730a13d1382995c2307fbcaa # v0.5.7
25
+ uses: zizmorcore/zizmor-action@6599ee8b7a49aef6a770f63d261d214911a7ce02 # v0.6.0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: fastapi-router-versioning
3
- Version: 0.2.7
3
+ Version: 0.3.0
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
@@ -49,7 +49,6 @@ Running multiple API versions side by side usually means duplicating routers, ha
49
49
  - **Latest alias**: expose the newest version under a fixed `/latest` prefix clients can pin to
50
50
  - **Self-hosted docs assets**: point Swagger UI and ReDoc at your own JS/CSS for air-gapped deployments
51
51
  - **Reverse proxy and sub-app aware**: doc URLs pick up the ASGI `root_path` at request time
52
- - **Configurable validation error status**: swap FastAPI's default `422` for any code, applied consistently to runtime responses and every OpenAPI schema, including the app's own root schema
53
52
  - **Route composition support**: works across nested routers, WebSockets, `Depends`, and OpenAPI Callbacks
54
53
 
55
54
  ---
@@ -189,8 +188,6 @@ Use one or the other: `deprecated=True` for an unconditional, version-independen
189
188
  | `redoc_js_url` | `str \| None` | FastAPI CDN | Custom URL for the ReDoc JS bundle |
190
189
  | `redoc_favicon_url` | `str \| None` | FastAPI favicon | Custom URL for the ReDoc favicon |
191
190
  | `redoc_with_google_fonts` | `bool` | `True` | Set `False` to stop ReDoc from loading Google Fonts |
192
- | `validation_error_code` | `int` | `422` | Status code returned for request validation errors; also replaces the `422` entry everywhere it appears in the OpenAPI schema, root schema included |
193
- | `handle_validation_exceptions` | `bool` | `True` | Set `False` to only patch the schema and register your own `RequestValidationError` handler |
194
191
 
195
192
  `.versionize()` returns the list of versions it activated. It can only be called once per
196
193
  instance; a second call raises `RuntimeError`, since it mutates the live FastAPI app in a way
@@ -299,6 +296,41 @@ RouterVersioner(
299
296
  ).versionize()
300
297
  ```
301
298
 
299
+ ### Custom validation error status code
300
+
301
+ Changing FastAPI's default `422` for request validation errors is not something
302
+ `RouterVersioner` does itself: it's a separate, general-purpose concern, handled by the
303
+ [fastapi-validation-override](https://pypi.org/project/fastapi-validation-override/) package.
304
+ `openapi_hook` is the integration point: it lets you re-apply the same patch to every
305
+ per-version schema that `RouterVersioner` generates, so all of them, root schema included,
306
+ stay consistent.
307
+
308
+ ```bash
309
+ uv add fastapi-validation-override
310
+ ```
311
+
312
+ ```python
313
+ from fastapi_validation_override import override_validation_error, patch_422_responses
314
+
315
+ # 1. Registers the runtime handler and patches the app's own root /openapi.json.
316
+ override_validation_error(app, status_code=400)
317
+
318
+ # 2. Re-applies the same patch to each version's own schema.
319
+ def versioning_openapi_hook(schema: dict, version: tuple[int, int]) -> dict:
320
+ return patch_422_responses(schema, "400")
321
+
322
+ RouterVersioner(
323
+ app=app,
324
+ routers=router,
325
+ version_format=VersionFormat.SEMVER,
326
+ openapi_hook=versioning_openapi_hook,
327
+ ).versionize()
328
+ # Validation failures now return 400, both at runtime and in every schema
329
+ # (root, and every /vX_Y/openapi.json)
330
+ ```
331
+
332
+ See [`examples/validation_override_integration_app.py`](https://github.com/mat81black/fastapi-router-versioning/blob/main/examples/validation_override_integration_app.py).
333
+
302
334
  ### OpenAPI Callbacks and Webhooks
303
335
 
304
336
  Route-level **Callbacks** need no special handling: a `callbacks=[...]` argument on a route
@@ -382,63 +414,18 @@ RouterVersioner(
382
414
  [`examples/download_static_assets.py`](https://github.com/mat81black/fastapi-router-versioning/blob/main/examples/download_static_assets.py) downloads the required files in one step;
383
415
  [`examples/self_hosted_docs_app.py`](https://github.com/mat81black/fastapi-router-versioning/blob/main/examples/self_hosted_docs_app.py) wires them into a full app.
384
416
 
385
- ### Validation error status code
386
-
387
- `validation_error_code` changes what FastAPI returns for a failed request body/query/path
388
- validation, `422` by default, both at runtime and in the schema:
389
-
390
- ```python
391
- RouterVersioner(
392
- app=app,
393
- routers=router,
394
- version_format=VersionFormat.SEMVER,
395
- validation_error_code=400,
396
- ).versionize()
397
- # Validation failures now return 400; every 422 entry in the schema becomes 400
398
- ```
399
-
400
- Set `handle_validation_exceptions=False` to keep the schema change while writing your own
401
- response body:
402
-
403
- ```python
404
- from fastapi import Request
405
- from fastapi.exceptions import RequestValidationError
406
- from fastapi.responses import JSONResponse
407
-
408
- @app.exception_handler(RequestValidationError)
409
- async def my_handler(request: Request, exc: RequestValidationError) -> JSONResponse:
410
- return JSONResponse(status_code=400, content={"errors": exc.errors()})
411
-
412
- RouterVersioner(
413
- app=app,
414
- routers=router,
415
- version_format=VersionFormat.SEMVER,
416
- validation_error_code=400,
417
- handle_validation_exceptions=False, # schema updated; the handler above owns the response
418
- ).versionize()
419
- ```
420
-
421
- This also patches the app's own root `/docs`, `/redoc`, and `/openapi.json`, not just the
422
- versioned ones, so there's nothing left showing the stale `422`.
423
-
424
417
  Sharing one app across several `RouterVersioner` instances only makes sense for one reason:
425
418
  mixing `version_format` values, SemVer for one group of routes and CalVer for another, on
426
419
  the same app. Splitting modules that share a `version_format` doesn't need a second instance;
427
420
  pass them all to one `RouterVersioner` via `routers=[...]` instead (see
428
421
  [`multi_router_app.py`](https://github.com/mat81black/fastapi-router-versioning/blob/main/examples/multi_router_app.py)).
429
- If you do share an app across instances, two rules are enforced for you:
430
-
431
- - Every instance with `handle_validation_exceptions=True` must agree on
432
- `validation_error_code`. FastAPI's exception handler is registered once per app, not per
433
- router, so a mismatch raises `RuntimeError` when the second instance is constructed rather
434
- than silently keeping whichever handler was registered first.
435
- - Every instance needs its own `prefix_format`/`latest_prefix`. Two instances that resolve to
436
- the same prefix would otherwise overwrite each other's docs/openapi routes at the same
437
- path; this raises `RuntimeError` too.
422
+ If you do share an app across instances, one rule is enforced for you: every instance needs
423
+ its own `prefix_format`/`latest_prefix`. Two instances that resolve to the same prefix would
424
+ otherwise overwrite each other's docs/openapi routes at the same path; this raises
425
+ `RuntimeError`.
438
426
 
439
- For modules that genuinely don't need to coordinate at all, including a different
440
- `validation_error_code` each, mount them as separate FastAPI sub-applications instead (next
441
- section).
427
+ For modules that genuinely don't need to coordinate at all, mount them as separate FastAPI
428
+ sub-applications instead (next section).
442
429
 
443
430
  ### Reverse proxy and sub-application mounting
444
431
 
@@ -453,7 +440,7 @@ parent.mount("/api", app) # root_path="/api" is injected per request
453
440
 
454
441
  A mounted sub-application is a separate `FastAPI()` instance with its own `app.state`, so a
455
442
  `RouterVersioner` attached to it is entirely independent from one attached to the parent, or
456
- to another sub-application, no shared `validation_error_code`, no prefix coordination needed.
443
+ to another sub-application, no prefix coordination needed.
457
444
  See [`examples/mounted_subapps_app.py`](https://github.com/mat81black/fastapi-router-versioning/blob/main/examples/mounted_subapps_app.py).
458
445
 
459
446
  ### Callback hook
@@ -486,9 +473,8 @@ RouterVersioner(
486
473
  | [`multi_router_app.py`](https://github.com/mat81black/fastapi-router-versioning/blob/main/examples/multi_router_app.py) | Several routers versioned together under one instance |
487
474
  | [`self_hosted_docs_app.py`](https://github.com/mat81black/fastapi-router-versioning/blob/main/examples/self_hosted_docs_app.py) | Swagger UI and ReDoc served from local static assets |
488
475
  | [`openapi_hook_app.py`](https://github.com/mat81black/fastapi-router-versioning/blob/main/examples/openapi_hook_app.py) | Per-version OpenAPI schema edits via `openapi_hook` |
489
- | [`validation_error_code_app.py`](https://github.com/mat81black/fastapi-router-versioning/blob/main/examples/validation_error_code_app.py) | `400` instead of `422`, handled automatically by `RouterVersioner` |
490
- | [`validation_error_code_custom_handler_app.py`](https://github.com/mat81black/fastapi-router-versioning/blob/main/examples/validation_error_code_custom_handler_app.py) | Same, but with `handle_validation_exceptions=False` and a custom handler |
491
476
  | [`mounted_subapps_app.py`](https://github.com/mat81black/fastapi-router-versioning/blob/main/examples/mounted_subapps_app.py) | Independently versioned modules as separate `app.mount()` sub-applications |
477
+ | [`validation_override_integration_app.py`](https://github.com/mat81black/fastapi-router-versioning/blob/main/examples/validation_override_integration_app.py) | Custom validation error status code via `fastapi-validation-override` and `openapi_hook` |
492
478
 
493
479
  ---
494
480
 
@@ -18,7 +18,6 @@ Running multiple API versions side by side usually means duplicating routers, ha
18
18
  - **Latest alias**: expose the newest version under a fixed `/latest` prefix clients can pin to
19
19
  - **Self-hosted docs assets**: point Swagger UI and ReDoc at your own JS/CSS for air-gapped deployments
20
20
  - **Reverse proxy and sub-app aware**: doc URLs pick up the ASGI `root_path` at request time
21
- - **Configurable validation error status**: swap FastAPI's default `422` for any code, applied consistently to runtime responses and every OpenAPI schema, including the app's own root schema
22
21
  - **Route composition support**: works across nested routers, WebSockets, `Depends`, and OpenAPI Callbacks
23
22
 
24
23
  ---
@@ -158,8 +157,6 @@ Use one or the other: `deprecated=True` for an unconditional, version-independen
158
157
  | `redoc_js_url` | `str \| None` | FastAPI CDN | Custom URL for the ReDoc JS bundle |
159
158
  | `redoc_favicon_url` | `str \| None` | FastAPI favicon | Custom URL for the ReDoc favicon |
160
159
  | `redoc_with_google_fonts` | `bool` | `True` | Set `False` to stop ReDoc from loading Google Fonts |
161
- | `validation_error_code` | `int` | `422` | Status code returned for request validation errors; also replaces the `422` entry everywhere it appears in the OpenAPI schema, root schema included |
162
- | `handle_validation_exceptions` | `bool` | `True` | Set `False` to only patch the schema and register your own `RequestValidationError` handler |
163
160
 
164
161
  `.versionize()` returns the list of versions it activated. It can only be called once per
165
162
  instance; a second call raises `RuntimeError`, since it mutates the live FastAPI app in a way
@@ -268,6 +265,41 @@ RouterVersioner(
268
265
  ).versionize()
269
266
  ```
270
267
 
268
+ ### Custom validation error status code
269
+
270
+ Changing FastAPI's default `422` for request validation errors is not something
271
+ `RouterVersioner` does itself: it's a separate, general-purpose concern, handled by the
272
+ [fastapi-validation-override](https://pypi.org/project/fastapi-validation-override/) package.
273
+ `openapi_hook` is the integration point: it lets you re-apply the same patch to every
274
+ per-version schema that `RouterVersioner` generates, so all of them, root schema included,
275
+ stay consistent.
276
+
277
+ ```bash
278
+ uv add fastapi-validation-override
279
+ ```
280
+
281
+ ```python
282
+ from fastapi_validation_override import override_validation_error, patch_422_responses
283
+
284
+ # 1. Registers the runtime handler and patches the app's own root /openapi.json.
285
+ override_validation_error(app, status_code=400)
286
+
287
+ # 2. Re-applies the same patch to each version's own schema.
288
+ def versioning_openapi_hook(schema: dict, version: tuple[int, int]) -> dict:
289
+ return patch_422_responses(schema, "400")
290
+
291
+ RouterVersioner(
292
+ app=app,
293
+ routers=router,
294
+ version_format=VersionFormat.SEMVER,
295
+ openapi_hook=versioning_openapi_hook,
296
+ ).versionize()
297
+ # Validation failures now return 400, both at runtime and in every schema
298
+ # (root, and every /vX_Y/openapi.json)
299
+ ```
300
+
301
+ See [`examples/validation_override_integration_app.py`](https://github.com/mat81black/fastapi-router-versioning/blob/main/examples/validation_override_integration_app.py).
302
+
271
303
  ### OpenAPI Callbacks and Webhooks
272
304
 
273
305
  Route-level **Callbacks** need no special handling: a `callbacks=[...]` argument on a route
@@ -351,63 +383,18 @@ RouterVersioner(
351
383
  [`examples/download_static_assets.py`](https://github.com/mat81black/fastapi-router-versioning/blob/main/examples/download_static_assets.py) downloads the required files in one step;
352
384
  [`examples/self_hosted_docs_app.py`](https://github.com/mat81black/fastapi-router-versioning/blob/main/examples/self_hosted_docs_app.py) wires them into a full app.
353
385
 
354
- ### Validation error status code
355
-
356
- `validation_error_code` changes what FastAPI returns for a failed request body/query/path
357
- validation, `422` by default, both at runtime and in the schema:
358
-
359
- ```python
360
- RouterVersioner(
361
- app=app,
362
- routers=router,
363
- version_format=VersionFormat.SEMVER,
364
- validation_error_code=400,
365
- ).versionize()
366
- # Validation failures now return 400; every 422 entry in the schema becomes 400
367
- ```
368
-
369
- Set `handle_validation_exceptions=False` to keep the schema change while writing your own
370
- response body:
371
-
372
- ```python
373
- from fastapi import Request
374
- from fastapi.exceptions import RequestValidationError
375
- from fastapi.responses import JSONResponse
376
-
377
- @app.exception_handler(RequestValidationError)
378
- async def my_handler(request: Request, exc: RequestValidationError) -> JSONResponse:
379
- return JSONResponse(status_code=400, content={"errors": exc.errors()})
380
-
381
- RouterVersioner(
382
- app=app,
383
- routers=router,
384
- version_format=VersionFormat.SEMVER,
385
- validation_error_code=400,
386
- handle_validation_exceptions=False, # schema updated; the handler above owns the response
387
- ).versionize()
388
- ```
389
-
390
- This also patches the app's own root `/docs`, `/redoc`, and `/openapi.json`, not just the
391
- versioned ones, so there's nothing left showing the stale `422`.
392
-
393
386
  Sharing one app across several `RouterVersioner` instances only makes sense for one reason:
394
387
  mixing `version_format` values, SemVer for one group of routes and CalVer for another, on
395
388
  the same app. Splitting modules that share a `version_format` doesn't need a second instance;
396
389
  pass them all to one `RouterVersioner` via `routers=[...]` instead (see
397
390
  [`multi_router_app.py`](https://github.com/mat81black/fastapi-router-versioning/blob/main/examples/multi_router_app.py)).
398
- If you do share an app across instances, two rules are enforced for you:
399
-
400
- - Every instance with `handle_validation_exceptions=True` must agree on
401
- `validation_error_code`. FastAPI's exception handler is registered once per app, not per
402
- router, so a mismatch raises `RuntimeError` when the second instance is constructed rather
403
- than silently keeping whichever handler was registered first.
404
- - Every instance needs its own `prefix_format`/`latest_prefix`. Two instances that resolve to
405
- the same prefix would otherwise overwrite each other's docs/openapi routes at the same
406
- path; this raises `RuntimeError` too.
391
+ If you do share an app across instances, one rule is enforced for you: every instance needs
392
+ its own `prefix_format`/`latest_prefix`. Two instances that resolve to the same prefix would
393
+ otherwise overwrite each other's docs/openapi routes at the same path; this raises
394
+ `RuntimeError`.
407
395
 
408
- For modules that genuinely don't need to coordinate at all, including a different
409
- `validation_error_code` each, mount them as separate FastAPI sub-applications instead (next
410
- section).
396
+ For modules that genuinely don't need to coordinate at all, mount them as separate FastAPI
397
+ sub-applications instead (next section).
411
398
 
412
399
  ### Reverse proxy and sub-application mounting
413
400
 
@@ -422,7 +409,7 @@ parent.mount("/api", app) # root_path="/api" is injected per request
422
409
 
423
410
  A mounted sub-application is a separate `FastAPI()` instance with its own `app.state`, so a
424
411
  `RouterVersioner` attached to it is entirely independent from one attached to the parent, or
425
- to another sub-application, no shared `validation_error_code`, no prefix coordination needed.
412
+ to another sub-application, no prefix coordination needed.
426
413
  See [`examples/mounted_subapps_app.py`](https://github.com/mat81black/fastapi-router-versioning/blob/main/examples/mounted_subapps_app.py).
427
414
 
428
415
  ### Callback hook
@@ -455,9 +442,8 @@ RouterVersioner(
455
442
  | [`multi_router_app.py`](https://github.com/mat81black/fastapi-router-versioning/blob/main/examples/multi_router_app.py) | Several routers versioned together under one instance |
456
443
  | [`self_hosted_docs_app.py`](https://github.com/mat81black/fastapi-router-versioning/blob/main/examples/self_hosted_docs_app.py) | Swagger UI and ReDoc served from local static assets |
457
444
  | [`openapi_hook_app.py`](https://github.com/mat81black/fastapi-router-versioning/blob/main/examples/openapi_hook_app.py) | Per-version OpenAPI schema edits via `openapi_hook` |
458
- | [`validation_error_code_app.py`](https://github.com/mat81black/fastapi-router-versioning/blob/main/examples/validation_error_code_app.py) | `400` instead of `422`, handled automatically by `RouterVersioner` |
459
- | [`validation_error_code_custom_handler_app.py`](https://github.com/mat81black/fastapi-router-versioning/blob/main/examples/validation_error_code_custom_handler_app.py) | Same, but with `handle_validation_exceptions=False` and a custom handler |
460
445
  | [`mounted_subapps_app.py`](https://github.com/mat81black/fastapi-router-versioning/blob/main/examples/mounted_subapps_app.py) | Independently versioned modules as separate `app.mount()` sub-applications |
446
+ | [`validation_override_integration_app.py`](https://github.com/mat81black/fastapi-router-versioning/blob/main/examples/validation_override_integration_app.py) | Custom validation error status code via `fastapi-validation-override` and `openapi_hook` |
461
447
 
462
448
  ---
463
449
 
@@ -2,6 +2,18 @@
2
2
 
3
3
  ## Latest Changes
4
4
 
5
+ ## 0.3.0 (2026-07-28)
6
+
7
+ ### Breaking Changes
8
+
9
+ * ♻️ Remove built-in validation error override in favor of fastapi-validation-override. PR [#46](https://github.com/mat81black/fastapi-router-versioning/pull/46) by [@mat81black](https://github.com/mat81black).
10
+
11
+ ### Internal
12
+
13
+ * ⬆ Bump fastapi from 0.139.0 to 0.139.2. PR [#49](https://github.com/mat81black/fastapi-router-versioning/pull/49) by [@dependabot[bot]](https://github.com/apps/dependabot).
14
+ * ⬆ Bump the python-packages group with 7 updates. PR [#48](https://github.com/mat81black/fastapi-router-versioning/pull/48) by [@dependabot[bot]](https://github.com/apps/dependabot).
15
+ * ⬆ Bump the github-actions group with 5 updates. PR [#47](https://github.com/mat81black/fastapi-router-versioning/pull/47) by [@dependabot[bot]](https://github.com/apps/dependabot).
16
+
5
17
  ## 0.2.7 (2026-07-18)
6
18
 
7
19
  ### Internal
@@ -20,7 +20,7 @@ class CreateItemRequest(BaseModel):
20
20
 
21
21
  main_app = FastAPI(title="Main App")
22
22
 
23
- # ── Admin sub-app: SemVer, validation errors return 400 ──────────────────────
23
+ # ── Admin sub-app: SemVer ──────────────────────────────────────────────────
24
24
 
25
25
  admin_app = FastAPI(title="Admin API")
26
26
  admin_router = APIRouter()
@@ -38,9 +38,7 @@ def create_admin_item(body: CreateItemRequest) -> dict[str, str]:
38
38
  return {"name": body.name, "quantity": str(body.quantity)}
39
39
 
40
40
 
41
- RouterVersioner(
42
- app=admin_app, routers=admin_router, version_format=VersionFormat.SEMVER, validation_error_code=400
43
- ).versionize()
41
+ RouterVersioner(app=admin_app, routers=admin_router, version_format=VersionFormat.SEMVER).versionize()
44
42
 
45
43
 
46
44
  # ── Orders sub-app: CalVer ────────────────────────────────────────────────────
@@ -0,0 +1,56 @@
1
+ """
2
+ Custom validation error status via fastapi-validation-override.
3
+
4
+ RouterVersioner does not have a built-in way to change FastAPI's default 422
5
+ status code: that is a separate, general-purpose concern handled by the
6
+ fastapi-validation-override package. This example wires the two together:
7
+
8
+ 1. override_validation_error() patches the app once, at runtime and on its own
9
+ root /openapi.json.
10
+ 2. openapi_hook re-applies patch_422_responses() to the schema RouterVersioner
11
+ generates for each version, so every versioned /vX_Y/openapi.json reflects
12
+ the same status code too.
13
+ """
14
+
15
+ from typing import Any
16
+
17
+ from fastapi import APIRouter, FastAPI
18
+ from fastapi_validation_override import override_validation_error, patch_422_responses
19
+
20
+ from fastapi_router_versioning import RouterVersioner, VersionFormat, VersionT, api_version
21
+
22
+ app = FastAPI(title="Validation Override Integration Demo")
23
+ router = APIRouter()
24
+
25
+
26
+ @router.get("/items")
27
+ @api_version((1, 0))
28
+ def get_items_v1(count: int) -> dict[str, str]:
29
+ return {"count": str(count)}
30
+
31
+
32
+ @router.get("/items")
33
+ @api_version((2, 0))
34
+ def get_items_v2(count: int) -> dict[str, str]:
35
+ return {"count": str(count)}
36
+
37
+
38
+ override_validation_error(app, status_code=400)
39
+
40
+
41
+ def versioning_openapi_hook(schema: dict[str, Any], version: VersionT) -> dict[str, Any]:
42
+ return patch_422_responses(schema, "400")
43
+
44
+
45
+ versioner = RouterVersioner(
46
+ app=app,
47
+ routers=router,
48
+ version_format=VersionFormat.SEMVER,
49
+ openapi_hook=versioning_openapi_hook,
50
+ )
51
+ versioner.versionize()
52
+
53
+ if __name__ == "__main__":
54
+ import uvicorn
55
+
56
+ uvicorn.run(app, host="127.0.0.1", port=8010)
@@ -1,5 +1,5 @@
1
1
  from .versioner import RouterVersioner, VersionFormat, VersionT, api_version
2
2
 
3
- __version__ = "0.2.7"
3
+ __version__ = "0.3.0"
4
4
 
5
5
  __all__ = ["RouterVersioner", "api_version", "VersionFormat", "VersionT"]