Repository: apiflask/apiflask
Pull request #346 (2022-08-29 02:26:07)

Change the default validation error status code to 422


- fixes [#345](https://github.com/apiflask/apiflask/issues/345)


Checklist:

- [x] Add tests that demonstrate the correct behavior of the change. Tests should fail without the change.
- [x] Add or update relevant docs, in the `docs` folder and in code docstring.
- [x] Add an entry in `CHANGES.md` summarizing the change and linking to the issue and your username.
- [x] Add `*Version changed*` or `*Version added*` note in any relevant docs and docstring.
- [x] Run `pytest` and `tox`, no tests failed.


greyli: Please update the changelog and add a `Version changed` section to the bottom of the `settings.py` file.

Patch:
diff --git a/CHANGES.md b/CHANGES.md
index 79cff87..0aed1cb 100644
--- a/CHANGES.md
+++ b/CHANGES.md
@@ -8,9 +8,11 @@ Released: -
 - Add `Enum` field from marshmallow 3.18.
 - Fix OpenAPI spec generating for path parameters when path schema is provided ([issue #350][issue_350]).
 - Add `spec_plugins` param to `APIFlask` class to support using custom apispec plugins ([issue #349][issue_349]).
+- Change the status code of request validation error from 400 to 422 ([issue #345][issue_345]).
 
 [issue_350]: https://github.com/apiflask/apiflask/issues/350
 [issue_349]: https://github.com/apiflask/apiflask/issues/349
+[issue_345]: https://github.com/apiflask/apiflask/issues/345
 
 
 ## Version 1.1.3
diff --git a/docs/configuration.md b/docs/configuration.md
index 6b01fd1..9460fe8 100644
--- a/docs/configuration.md
+++ b/docs/configuration.md
@@ -711,7 +711,7 @@ app.config['NOT_FOUND_DESCRIPTION'] = 'Missing'
 The status code of validation error response.
 
 - Type: `int`
-- Default value: `400`
+- Default value: `422`
 - Examples:
 
 ```python
diff --git a/docs/error-handling.md b/docs/error-handling.md
index 3b842b5..b23b470 100644
--- a/docs/error-handling.md
+++ b/docs/error-handling.md
@@ -231,7 +231,7 @@ The error object is an instance of [`HTTPError`][apiflask.exceptions.HTTPError],
 so you can get error information via its attributes:
 
 - status_code: If the error is triggered by a validation error, the value will be
-    400 (default) or the value you passed in config `VALIDATION_ERROR_STATUS_CODE`.
+    422 (default) or the value you passed in config `VALIDATION_ERROR_STATUS_CODE`.
     If the error is triggered by [`HTTPError`][apiflask.exceptions.HTTPError]
     or [`abort`][apiflask.exceptions.abort], it will be the status code
     you passed. Otherwise, it will be the status code set by Werkzueg when
diff --git a/docs/request.md b/docs/request.md
index 2a16ef2..07aa396 100644
--- a/docs/request.md
+++ b/docs/request.md
@@ -108,7 +108,7 @@ The value of `<location>` is where the validation error happened.
 
 - status code
 
-The default status code of validation error is 400, you can change this via the
+The default status code of validation error is 422, you can change this via the
 config `VALIDATION_ERROR_STATUS_CODE`.
 
 
diff --git a/src/apiflask/app.py b/src/apiflask/app.py
index 4c33d2d..8acabbe 100644
--- a/src/apiflask/app.py
+++ b/src/apiflask/app.py
@@ -513,7 +513,7 @@ class APIFlask(APIScaffold, Flask):
         so you can get error information via it's attributes:
 
         - status_code: If the error is triggered by a validation error, the value will be
-          400 (default) or the value you passed in config `VALIDATION_ERROR_STATUS_CODE`.
+          422 (default) or the value you passed in config `VALIDATION_ERROR_STATUS_CODE`.
           If the error is triggered by [`HTTPError`][apiflask.exceptions.HTTPError]
           or [`abort`][apiflask.exceptions.abort], it will be the status code
           you passed. Otherwise, it will be the status code set by Werkzueg when
diff --git a/src/apiflask/settings.py b/src/apiflask/settings.py
index 2cab8a4..82054f9 100644
--- a/src/apiflask/settings.py
+++ b/src/apiflask/settings.py
@@ -38,7 +38,7 @@ SUCCESS_DESCRIPTION: str = 'Successful response'
 NOT_FOUND_DESCRIPTION: str = 'Not found'
 VALIDATION_ERROR_DESCRIPTION: str = 'Validation error'
 AUTH_ERROR_DESCRIPTION: str = 'Authentication error'
-VALIDATION_ERROR_STATUS_CODE: int = 400
+VALIDATION_ERROR_STATUS_CODE: int = 422
 AUTH_ERROR_STATUS_CODE: int = 401
 VALIDATION_ERROR_SCHEMA: OpenAPISchemaType = validation_error_schema
 HTTP_ERROR_SCHEMA: OpenAPISchemaType = http_error_schema
@@ -67,3 +67,6 @@ RAPIDOC_THEME: str = 'light'
 RAPIDOC_CONFIG: t.Optional[dict] = None
 RAPIPDF_JS: str = 'https://unpkg.com/rapipdf/dist/rapipdf-min.js'
 RAPIPDF_CONFIG: t.Optional[dict] = None
+
+# Version changed: 1.2.0
+# Change VALIDATION_ERROR_STATUS_CODE from 400 to 422.


Test patch:
diff --git a/examples/test_examples.py b/examples/test_examples.py
index c2d4705..7ccabe3 100644
--- a/examples/test_examples.py
+++ b/examples/test_examples.py
@@ -108,7 +108,7 @@ def test_create_pet(client):
 ])
 def test_create_pet_with_bad_data(client, data):
     rv = client.post('/pets', json=data)
-    assert rv.status_code == 400
+    assert rv.status_code == 422
     assert rv.json
 
 
@@ -144,7 +144,7 @@ def test_update_pet(client):
 ])
 def test_update_pet_with_bad_data(client, data):
     rv = client.patch('/pets/1', json=data)
-    assert rv.status_code == 400
+    assert rv.status_code == 422
     assert rv.json
 
 
@@ -194,7 +194,7 @@ def test_get_pets_pagination(client):
 @pytest.mark.parametrize('client', ['pagination'], indirect=True)
 def test_get_pets_pagination_with_bad_data(client):
     rv = client.get('/pets?per_page=100')
-    assert rv.status_code == 400
+    assert rv.status_code == 422
 
     rv = client.get('/pets?page=100')
     assert rv.status_code == 404
diff --git a/tests/test_decorator_doc.py b/tests/test_decorator_doc.py
index 9fb435e..182739a 100644
--- a/tests/test_decorator_doc.py
+++ b/tests/test_decorator_doc.py
@@ -194,7 +194,7 @@ def test_doc_responses(app, client):
     assert rv.json['paths']['/bar']['get']['responses'][
         '200']['description'] == 'Successful response'
     assert rv.json['paths']['/bar']['get']['responses'][
-        '400']['description'] == 'Validation error'
+        '400']['description'] == 'Bad Request'
     assert '404' in rv.json['paths']['/foo']['get']['responses']
     assert rv.json['paths']['/bar']['get']['responses'][
         '404']['description'] == 'Not Found'
@@ -242,7 +242,7 @@ def test_doc_responses_with_methodview(app, client):
     assert rv.json['paths']['/bar']['get']['responses'][
         '200']['description'] == 'Successful response'
     assert rv.json['paths']['/bar']['get']['responses'][
-        '400']['description'] == 'Validation error'
+        '400']['description'] == 'Bad Request'
     assert '404' in rv.json['paths']['/foo']['get']['responses']
     assert rv.json['paths']['/bar']['get']['responses'][
         '404']['description'] == 'Not Found'
diff --git a/tests/test_decorator_input.py b/tests/test_decorator_input.py
index 8341df5..cf4230b 100644
--- a/tests/test_decorator_input.py
+++ b/tests/test_decorator_input.py
@@ -29,7 +29,7 @@ def test_input(app, client):
 
     for rule in ['/foo', '/bar']:
         rv = client.post(rule)
-        assert rv.status_code == 400
+        assert rv.status_code == 422
         assert rv.json == {
             'detail': {
                 'json': {'name': ['Missing data for required field.']}
@@ -38,7 +38,7 @@ def test_input(app, client):
         }
 
         rv = client.post(rule, json={'id': 1})
-        assert rv.status_code == 400
+        assert rv.status_code == 422
         assert rv.json == {
             'detail': {
                 'json': {'name': ['Missing data for required field.']}
@@ -69,7 +69,7 @@ def test_input_with_query_location(app, client):
         return {'name': schema['name'], 'name2': schema2['name2']}
 
     rv = client.post('/foo')
-    assert rv.status_code == 400
+    assert rv.status_code == 422
     assert rv.json == {
         'detail': {
             'query': {'name': ['Missing data for required field.']}
@@ -78,7 +78,7 @@ def test_input_with_query_location(app, client):
     }
 
     rv = client.post('/foo?id=1&name=bar')
-    assert rv.status_code == 400
+    assert rv.status_code == 422
     assert rv.json == {
         'detail': {
             'query': {'name2': ['Missing data for required field.']}
@@ -207,7 +207,7 @@ def test_input_with_path_location(app, client):
     assert rv.json == {'image_type': 'png'}
 
     rv = client.get('/gif')
-    assert rv.status_code == 400
+    assert rv.status_code == 422
     assert rv.json['message'] == 'Validation error'
     assert 'path' in rv.json['detail']
     assert 'image_type' in rv.json['detail']['path']
@@ -269,7 +269,7 @@ def test_input_with_dict_schema(app, client):
         return body
 
     rv = client.get('/foo')
-    assert rv.status_code == 400
+    assert rv.status_code == 422
     assert rv.json == {
         'detail': {
             'query': {'name': ['Missing data for required field.']}
@@ -282,7 +282,7 @@ def test_input_with_dict_schema(app, client):
     assert rv.json == {'name': 'grey'}
 
     rv = client.post('/bar')
-    assert rv.status_code == 400
+    assert rv.status_code == 422
     assert rv.json == {
         'detail': {
             'json': {'name': ['Missing data for required field.']}
diff --git a/tests/test_fields.py b/tests/test_fields.py
index 50e3712..dff513b 100644
--- a/tests/test_fields.py
+++ b/tests/test_fields.py
@@ -38,7 +38,7 @@ def test_file_field(app, client):
         },
         content_type='multipart/form-data'
     )
-    assert rv.status_code == 400
+    assert rv.status_code == 422
     assert rv.json['detail']['files']['image'] == ['Not a valid file.']
 
 
@@ -77,5 +77,5 @@ def test_multiple_file_field(app, client):
         },
         content_type='multipart/form-data'
     )
-    assert rv.status_code == 400
+    assert rv.status_code == 422
     assert rv.json['detail']['files']['images']['2'] == ['Not a valid file.']
diff --git a/tests/test_settings_auto_behaviour.py b/tests/test_settings_auto_behaviour.py
index 90e6f73..46a2730 100644
--- a/tests/test_settings_auto_behaviour.py
+++ b/tests/test_settings_auto_behaviour.py
@@ -238,11 +238,11 @@ def test_auto_validation_error_response(app, client, config_value):
     rv = client.get('/openapi.json')
     assert rv.status_code == 200
     validate_spec(rv.json)
-    assert bool('400' in rv.json['paths']['/foo']['post']['responses']) is config_value
+    assert bool('422' in rv.json['paths']['/foo']['post']['responses']) is config_value
     if config_value:
         assert 'ValidationError' in rv.json['components']['schemas']
         assert '#/components/schemas/ValidationError' in \
-            rv.json['paths']['/foo']['post']['responses']['400'][
+            rv.json['paths']['/foo']['post']['responses']['422'][
                 'content']['application/json']['schema']['$ref']
 
 
diff --git a/tests/test_settings_response_customization.py b/tests/test_settings_response_customization.py
index c085dce..2737e48 100644
--- a/tests/test_settings_response_customization.py
+++ b/tests/test_settings_response_customization.py
@@ -53,7 +53,7 @@ def test_response_description_config(app, client):
 
 
 def test_validation_error_status_code_and_description(app, client):
-    app.config['VALIDATION_ERROR_STATUS_CODE'] = 422
+    app.config['VALIDATION_ERROR_STATUS_CODE'] = 400
     app.config['VALIDATION_ERROR_DESCRIPTION'] = 'Bad'
 
     @app.post('/foo')
@@ -64,9 +64,9 @@ def test_validation_error_status_code_and_description(app, client):
     rv = client.get('/openapi.json')
     assert rv.status_code == 200
     validate_spec(rv.json)
-    assert rv.json['paths']['/foo']['post']['responses']['422'] is not None
+    assert rv.json['paths']['/foo']['post']['responses']['400'] is not None
     assert rv.json['paths']['/foo']['post']['responses'][
-        '422']['description'] == 'Bad'
+        '400']['description'] == 'Bad'
 
 
 @pytest.mark.parametrize('schema', [
@@ -84,8 +84,8 @@ def test_validation_error_schema(app, client, schema):
     rv = client.get('/openapi.json')
     assert rv.status_code == 200
     validate_spec(rv.json)
-    assert rv.json['paths']['/foo']['post']['responses']['400']
-    assert rv.json['paths']['/foo']['post']['responses']['400'][
+    assert rv.json['paths']['/foo']['post']['responses']['422']
+    assert rv.json['paths']['/foo']['post']['responses']['422'][
         'description'] == 'Validation error'
     assert 'ValidationError' in rv.json['components']['schemas']