Skip to content

Commit 71ff420

Browse files
committed
WIP: Dictionary (JSON) Validation from Json and Query
1 parent 5b41b6b commit 71ff420

File tree

4 files changed

+57
-9
lines changed

4 files changed

+57
-9
lines changed

flask_parameter_validation/parameter_types/parameter.py

Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,10 +2,13 @@
22
Base Parameter class.
33
Should only be used as child class for other params.
44
"""
5+
import json
56
import re
67
from datetime import date, datetime, time
78

89
import dateutil.parser as parser
10+
import jsonschema
11+
from jsonschema.exceptions import ValidationError as JSONSchemaValidationError
912

1013

1114
class Parameter:
@@ -27,6 +30,7 @@ def __init__(
2730
datetime_format=None, # str: datetime format string (https://docs.python.org/3/library/datetime.html#strftime-and-strptime-format-codes),
2831
comment=None, # str: comment for autogenerated documentation
2932
alias=None, # str: alias for parameter name
33+
json_schema=None, # dict: JSON Schema to check received dicts or lists against
3034
):
3135
self.default = default
3236
self.min_list_length = min_list_length
@@ -42,9 +46,11 @@ def __init__(
4246
self.datetime_format = datetime_format
4347
self.comment = comment
4448
self.alias = alias
49+
self.json_schema = json_schema
4550

4651
# Validator
4752
def validate(self, value):
53+
print(f"Parameter#validate received {type(value)}")
4854
if type(value) is list:
4955
values = value
5056
# Min list len
@@ -59,6 +65,21 @@ def validate(self, value):
5965
raise ValueError(
6066
f"must have have a maximum of {self.max_list_length} items."
6167
)
68+
if self.json_schema is not None:
69+
try:
70+
jsonschema.validate(value, self.json_schema)
71+
print("JSON Schema Passed")
72+
except JSONSchemaValidationError as e:
73+
raise ValueError(f"failed JSON Schema validation: {e.args[0]}")
74+
elif type(value) is dict:
75+
print("dict validate path")
76+
if self.json_schema is not None:
77+
try:
78+
jsonschema.validate(value, self.json_schema)
79+
except JSONSchemaValidationError as e:
80+
print("Failed JSON Schema")
81+
raise ValueError(f"failed JSON Schema validation: {e.args[0]}")
82+
values = [value]
6283
else:
6384
values = [value]
6485

@@ -133,6 +154,7 @@ def validate(self, value):
133154

134155
def convert(self, value, allowed_types):
135156
"""Some parameter types require manual type conversion (see Query)"""
157+
print(f"Parameter#convert received {type(value)}")
136158
# Datetime conversion
137159
if None in allowed_types and value is None:
138160
return value
@@ -160,4 +182,5 @@ def convert(self, value, allowed_types):
160182
return date.fromisoformat(str(value))
161183
except ValueError:
162184
raise ValueError("date format does not match ISO 8601")
185+
print(f"Parameter#convert returned {type(value)}")
163186
return value

flask_parameter_validation/parameter_types/query.py

Lines changed: 10 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,8 @@
22
Query Parameters
33
- i.e. sent in GET requests, /?username=myname
44
"""
5+
import json
6+
57
from .parameter import Parameter
68

79

@@ -13,6 +15,7 @@ def __init__(self, default=None, **kwargs):
1315

1416
def convert(self, value, allowed_types):
1517
"""Convert query parameters to corresponding types."""
18+
print(f"Query#convert received {type(value)}")
1619
if type(value) is str:
1720
# int conversion
1821
if int in allowed_types:
@@ -32,5 +35,11 @@ def convert(self, value, allowed_types):
3235
value = True
3336
elif value.lower() == "false":
3437
value = False
35-
38+
# dict conversion
39+
if dict in allowed_types:
40+
try:
41+
value = json.loads(value)
42+
except ValueError:
43+
raise ValueError(f"invalid JSON")
44+
print(f"Query#convert returned {type(value)}")
3645
return super().convert(value, allowed_types)

flask_parameter_validation/parameter_validation.py

Lines changed: 23 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
import asyncio
22
import functools
33
import inspect
4+
import json
45
import re
56
from inspect import signature
67

@@ -46,7 +47,7 @@ def __call__(self, f):
4647
async def nested_func(**kwargs):
4748
# Step 1 - Get expected input details as dict
4849
expected_inputs = signature(f).parameters
49-
50+
5051
# Step 2 - Validate JSON inputs
5152
json_input = None
5253
if request.headers.get("Content-Type") is not None:
@@ -61,7 +62,8 @@ async def nested_func(**kwargs):
6162
# Step 3 - Extract list of parameters expected to be lists (otherwise all values are converted to lists)
6263
expected_list_params = []
6364
for name, param in expected_inputs.items():
64-
if str(param.annotation).startswith("typing.List") or str(param.annotation).startswith("typing.Optional[typing.List"):
65+
if str(param.annotation).startswith("typing.List") or str(param.annotation).startswith(
66+
"typing.Optional[typing.List"):
6567
expected_list_params.append(param.default.alias or name)
6668

6769
# Step 4 - Convert request inputs to dicts
@@ -152,7 +154,7 @@ def validate(self, expected_input, all_request_inputs):
152154
else:
153155
# Optionals are Unions with a NoneType, so we should check if None is part of Union __args__ (if exist)
154156
if (
155-
hasattr(expected_input_type, "__args__") and type(None) in expected_input_type.__args__
157+
hasattr(expected_input_type, "__args__") and type(None) in expected_input_type.__args__
156158
):
157159
return user_input
158160
else:
@@ -202,6 +204,9 @@ def validate(self, expected_input, all_request_inputs):
202204
user_inputs[count] = expected_delivery_type.convert(
203205
value, expected_input_types
204206
)
207+
for t in expected_input_types:
208+
print(f"Expected type: {t}")
209+
print(f"Appended value of type {type(user_inputs[count])} to user_inputs")
205210
except ValueError as e:
206211
raise ValidationError(str(e), expected_name, expected_input_type)
207212

@@ -215,6 +220,8 @@ def validate(self, expected_input, all_request_inputs):
215220
if type(user_input) is not list:
216221
validation_success = False
217222

223+
print(f"Validation Success? {validation_success}")
224+
218225
# Error if types don't match
219226
if not validation_success:
220227
if hasattr(
@@ -229,11 +236,19 @@ def validate(self, expected_input, all_request_inputs):
229236
original_expected_input_type,
230237
)
231238

232-
# Validate parameter-specific requirements are met
233-
try:
234-
expected_delivery_type.validate(user_input)
235-
except ValueError as e:
236-
raise ValidationError(str(e), expected_name, expected_input_type)
239+
print(json.dumps(user_input))
240+
241+
if expected_input_type_str.startswith("typing.List"):
242+
# Validate parameter-specific requirements are met
243+
try:
244+
expected_delivery_type.validate(user_input)
245+
except ValueError as e:
246+
raise ValidationError(str(e), expected_name, expected_input_type)
247+
else:
248+
try:
249+
expected_delivery_type.validate(user_inputs[0])
250+
except ValueError as e:
251+
raise ValidationError(str(e), expected_name, expected_input_type)
237252

238253
# Return input back to parent function
239254
if expected_input_type_str.startswith("typing.List"):

setup.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -30,6 +30,7 @@
3030
'Flask',
3131
'flask[async]',
3232
'python-dateutil',
33+
'jsonschema',
3334
],
3435
classifiers=[
3536
'Environment :: Web Environment',

0 commit comments

Comments
 (0)