Skip to content

Commit 0371327

Browse files
authored
Merge pull request #38 from LeanderCS/37
35 | Deserialization
2 parents 46df26d + 1c380b3 commit 0371327

File tree

11 files changed

+311
-44
lines changed

11 files changed

+311
-44
lines changed

docs/source/changelog.rst

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,23 @@ Changelog
33

44
All notable changes to this project will be documented in this file.
55

6+
[0.3.0] - 2025-04-10
7+
--------------------
8+
9+
Added
10+
^^^^^
11+
12+
13+
Changed
14+
^^^^^^^
15+
- Updated ``IsTypedDictValidator` and ``IsDataclassValidator`` to require a specific model and
16+
checks if the input json is in the defined format.
17+
18+
Removed
19+
^^^^^^^
20+
- ``RemoveEmojisFilter``
21+
- ``ToPascaleCaseFilter``
22+
623

724
[0.2.0] - 2025-04-07
825
--------------------

docs/source/conf.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
project = "flask-inputfilter"
22
copyright = "2025, Leander Cain Slotosch"
33
author = "Leander Cain Slotosch"
4-
release = "0.2.0"
4+
release = "0.3.0"
55

66
extensions = ["sphinx_rtd_theme"]
77

Lines changed: 109 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,109 @@
1+
Deserialization
2+
===============
3+
4+
Deserialization in Flask-InputFilter allows you to convert validated data
5+
into custom model objects or maintain it as a dictionary. This feature is
6+
particularly useful when you want to work with strongly-typed objects in
7+
your application.
8+
9+
Overview
10+
--------
11+
12+
The deserialization process is handled through two main methods:
13+
14+
- ``setModel()``: Sets the model class to be used for deserialization
15+
- ``serialize()``: Converts the validated data into an instance of the
16+
specified model class or returns the raw data as a dictionary
17+
18+
Configuration
19+
-------------
20+
21+
The ``validate()`` method will automatically deserialize the validated data
22+
into an instance of the model class, if there is a model class set.
23+
24+
.. code-block:: python
25+
26+
from flask_inputfilter import InputFilter
27+
from dataclasses import dataclass
28+
29+
30+
@dataclass
31+
class User:
32+
username: str
33+
email: str
34+
35+
36+
class UserInputFilter(InputFilter):
37+
def __init__(self):
38+
super().__init__()
39+
40+
self.setModel(User)
41+
42+
Examples
43+
--------
44+
45+
Usage with Flask Routes
46+
^^^^^^^^^^^^^^^^^^^^^^^
47+
48+
You can also use deserialization in your Flask routes:
49+
50+
.. code-block:: python
51+
52+
from flask import Flask, jsonify, g
53+
from flask_inputfilter import InputFilter
54+
55+
56+
class User:
57+
def __init__(self, username: str):
58+
self.username = username
59+
60+
61+
class MyInputFilter(InputFilter):
62+
def __init__(self):
63+
super().__init__(methods=["GET"])
64+
self.add("username")
65+
self.setModel(User)
66+
67+
68+
app = Flask(__name__)
69+
70+
@app.route("/test", methods=["GET"])
71+
@MyInputFilter.validate()
72+
def test_route():
73+
# g.validated_data will contain the deserialized User instance
74+
75+
validated_data: User = g.validated_data
76+
77+
Usage outside of Flask Routes
78+
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
79+
80+
You can also use deserialization outside of Flask routes:
81+
82+
.. code-block:: python
83+
84+
from flask import Flask, jsonify, g
85+
from flask_inputfilter import InputFilter
86+
87+
88+
class User:
89+
def __init__(self, username: str):
90+
self.username = username
91+
92+
93+
class MyInputFilter(InputFilter):
94+
def __init__(self):
95+
super().__init__(methods=["GET"])
96+
self.add("username")
97+
self.setModel(User)
98+
99+
app = Flask(__name__)
100+
101+
@app.route("/test", methods=["GET"])
102+
def test_route():
103+
input_filter = MyInputFilter()
104+
input_filter.setData({"username": "test user"})
105+
106+
if not input_filter.isValid():
107+
return jsonify({"error": "Invalid data"}), 400
108+
109+
validated_data: User = input_filter.serialize()

docs/source/options/index.rst

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,3 +11,4 @@ Options
1111
condition
1212
copy
1313
external_api
14+
deserialization

docs/source/options/validator.rst

Lines changed: 6 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -504,16 +504,16 @@ IsDataclassValidator
504504
~~~~~~~~~~~~~~~~~~~~
505505
**Description:**
506506

507-
Validates that the provided value is an instance of a dataclass. Optionally checks whether it matches a specific dataclass type.
507+
Validates that the provided value conforms to a specific dataclass type.
508508

509509
**Parameters:**
510510

511-
- **dataclass_type** (*Optional[Type]*): The expected dataclass type.
511+
- **dataclass_type** (*Type[dict]*): The expected dataclass type.
512512
- **error_message** (*Optional[str]*): Custom error message if validation fails.
513513

514514
**Expected Behavior:**
515515

516-
Ensures the input is a dataclass (using Python’s dataclass mechanism) and, if specified, that it is an instance of the provided type. Raises a ``ValidationError`` otherwise.
516+
Ensures the input is a dictionary and, that all expected keys are present. Raises a ``ValidationError`` if the structure does not match.
517517

518518
**Example Usage:**
519519

@@ -974,16 +974,16 @@ IsTypedDictValidator
974974
~~~~~~~~~~~~~~~~~~~~
975975
**Description:**
976976

977-
Validates that the provided value is a TypedDict instance. Optionally, it checks whether the dictionary conforms to a specified TypedDict structure.
977+
Validates that the provided value conforms to a specified TypedDict structure.
978978

979979
**Parameters:**
980980

981-
- **typed_dict_type** (*Optional[Type[TypedDict]]*): The TypedDict class that defines the expected structure.
981+
- **typed_dict_type** (*Type[TypedDict]*): The TypedDict class that defines the expected structure.
982982
- **error_message** (*Optional[str]*): Custom error message if the validation fails.
983983

984984
**Expected Behavior:**
985985

986-
Ensures the input is a dictionary and, if a specific TypedDict type is provided, that all expected keys are present. Raises a ``ValidationError`` if the structure does not match.
986+
Ensures the input is a dictionary and, that all expected keys are present. Raises a ``ValidationError`` if the structure does not match.
987987

988988
**Example Usage:**
989989

flask_inputfilter/InputFilter.py

Lines changed: 47 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,16 @@
11
import json
22
import re
3-
from typing import Any, Callable, Dict, List, Optional, Tuple, Union
3+
from typing import (
4+
Any,
5+
Callable,
6+
Dict,
7+
List,
8+
Optional,
9+
Tuple,
10+
Type,
11+
TypeVar,
12+
Union,
13+
)
414

515
from flask import Response, g, request
616
from typing_extensions import final
@@ -13,6 +23,8 @@
1323

1424
API_PLACEHOLDER_PATTERN = re.compile(r"{{(.*?)}}")
1525

26+
T = TypeVar("T")
27+
1628

1729
class InputFilter:
1830
"""
@@ -28,6 +40,7 @@ class InputFilter:
2840
"__data",
2941
"__validated_data",
3042
"__errors",
43+
"__model_class",
3144
)
3245

3346
def __init__(self, methods: Optional[List[str]] = None) -> None:
@@ -39,6 +52,7 @@ def __init__(self, methods: Optional[List[str]] = None) -> None:
3952
self.__data: Dict[str, Any] = {}
4053
self.__validated_data: Dict[str, Any] = {}
4154
self.__errors: Dict[str, str] = {}
55+
self.__model_class: Optional[Type[T]] = None
4256

4357
@final
4458
def add(
@@ -333,7 +347,7 @@ def getValues(self) -> Dict[str, Any]:
333347
Retrieves a dictionary of key-value pairs from the current object.
334348
This method provides access to the internal state or configuration of
335349
the object in a dictionary format, where keys are strings and values
336-
can be of various types depending on the objects design.
350+
can be of various types depending on the object's design.
337351
338352
Returns:
339353
Dict[str, Any]: A dictionary containing string keys and their
@@ -518,6 +532,31 @@ def merge(self, other: "InputFilter") -> None:
518532
else:
519533
self.__global_validators.append(validator)
520534

535+
@final
536+
def setModel(self, model_class: Type[T]) -> None:
537+
"""
538+
Set the model class for serialization.
539+
540+
Args:
541+
model_class: The class to use for serialization.
542+
"""
543+
self.__model_class = model_class
544+
545+
@final
546+
def serialize(self) -> Union[Dict[str, Any], T]:
547+
"""
548+
Serialize the validated data. If a model class is set,
549+
returns an instance of that class, otherwise returns the
550+
raw validated data.
551+
552+
Returns:
553+
Union[Dict[str, Any], T]: The serialized data.
554+
"""
555+
if self.__model_class is None:
556+
return self.__validated_data
557+
558+
return self.__model_class(**self.__validated_data)
559+
521560
@final
522561
def isValid(self) -> bool:
523562
"""
@@ -649,7 +688,12 @@ def wrapper(
649688

650689
input_filter.__data = {**data, **kwargs}
651690

652-
g.validated_data = input_filter.validateData()
691+
validated_data = input_filter.validateData()
692+
693+
if input_filter.__model_class is not None:
694+
validated_data = input_filter.serialize()
695+
696+
g.validated_data = validated_data
653697

654698
except ValidationError as e:
655699
return Response(

flask_inputfilter/Validator/IsDataclassValidator.py

Lines changed: 8 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,10 @@
1-
from dataclasses import is_dataclass
2-
from typing import Any, Optional, Type
1+
from typing import Any, Optional, Type, TypeVar
32

43
from flask_inputfilter.Exception import ValidationError
54
from flask_inputfilter.Validator import BaseValidator
65

6+
T = TypeVar("T")
7+
78

89
class IsDataclassValidator(BaseValidator):
910
"""
@@ -14,20 +15,21 @@ class IsDataclassValidator(BaseValidator):
1415

1516
def __init__(
1617
self,
17-
dataclass_type: Optional[Type[dict]] = None,
18+
dataclass_type: Type[T],
1819
error_message: Optional[str] = None,
1920
) -> None:
2021
self.dataclass_type = dataclass_type
2122
self.error_message = error_message
2223

2324
def validate(self, value: Any) -> None:
24-
if not is_dataclass(value):
25+
if not isinstance(value, dict):
2526
raise ValidationError(
2627
self.error_message
27-
or "The provided value is not a dataclass instance."
28+
or "The provided value is not a dict instance."
2829
)
2930

30-
if self.dataclass_type and not isinstance(value, self.dataclass_type):
31+
expected_keys = self.dataclass_type.__annotations__.keys()
32+
if any(key not in value for key in expected_keys):
3133
raise ValidationError(
3234
self.error_message
3335
or f"'{value}' is not an instance "

flask_inputfilter/Validator/IsTypedDictValidator.py

Lines changed: 9 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -15,7 +15,7 @@ class IsTypedDictValidator(BaseValidator):
1515

1616
def __init__(
1717
self,
18-
typed_dict_type: Optional[Type[TypedDict]] = None,
18+
typed_dict_type: Type[TypedDict],
1919
error_message: Optional[str] = None,
2020
) -> None:
2121
self.typed_dict_type = typed_dict_type
@@ -25,14 +25,13 @@ def validate(self, value: Any) -> None:
2525
if not isinstance(value, dict):
2626
raise ValidationError(
2727
self.error_message
28-
or "The provided value is not a TypedDict instance."
28+
or "The provided value is not a dict instance."
2929
)
3030

31-
if self.typed_dict_type:
32-
expected_keys = self.typed_dict_type.__annotations__.keys()
33-
if not all(key in value for key in expected_keys):
34-
raise ValidationError(
35-
self.error_message
36-
or f"'{value}' does not match "
37-
f"{self.typed_dict_type.__name__} structure."
38-
)
31+
expected_keys = self.typed_dict_type.__annotations__.keys()
32+
if any(key not in value for key in expected_keys):
33+
raise ValidationError(
34+
self.error_message
35+
or f"'{value}' does not match "
36+
f"{self.typed_dict_type.__name__} structure."
37+
)

setup.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@
22

33
setup(
44
name="flask_inputfilter",
5-
version="0.2.0",
5+
version="0.3.0",
66
license="MIT",
77
author="Leander Cain Slotosch",
88
author_email="[email protected]",

0 commit comments

Comments
 (0)