Closed abadyan-vonage closed 5 years ago
I'll look a bit into the code (as i'm looking at API doc things now) but some quick thoughts:
1) json schema generally allows for extra parameters
The additionalProperties keyword is used to control the handling of extra stuff, that is, properties whose names are not listed in the properties keyword. By default any additional properties are allowed. json schema object information
2) be sure to set validate=True
for validation. That being said, 1 likely supersede's 2.
I'll look more into the code and see what's up. It may be that this requires an enhancement like json schema's additionalProperties: false
.
A code example would be very useful @abadyan-vonage 😊
@SteadBytes any kind of example would do here...
account_settings_model = api.model("AccountSettings", {"autoLogout": fields.Integer()})
If I have an endpoint that expects this model:
@api.route("/accounts/<int:account_id>/settings")
class AccountSettings(Resource):
@api.expect(account_settings_model)
def put(self, account_id: int):
print(request.json)
When this endpoint gets called with a json like this:
{"autoLogout": 4, "xxx": "yyy" }
The xxx
field does not get filtered out in any way or blocked.
I think the best approach would be to give me access to an object that only contains the fields from the model, since ideally I would not want to fail requests containing extra fields.
Thank you for your example, api.expect
is not intended to filter input payloads from request.json
in this manner. It will validate that the fields provided match the model if validate=True
is passed. As @j5awry mentioned, validation failure won't occur due to additional fields being present. However, it will fail if required fields are missing.
from flask import Flask, request
from flask_restplus import (
Api,
Namespace,
Resource,
fields,
reqparse,
)
app = Flask(__name__)
api = Api(app)
account_settings_model = api.model(
"AccountSettings",
{
"autoLogout": fields.Integer(required=True), # must be present for validation to pass
"accountID": fields.String, # optional
},
)
@api.route("/accounts/<int:account_id>/settings")
class AccountSettings(Resource):
@api.expect(account_settings_model, validate=True) # ensure payload validation
def put(self, account_id):
print(request.json)
if __name__ == "__main__":
app.run(port=8080, debug=True)
200 OK
{
"autoLogout": 4,
"accountID": "abcde"
}
Output in console:
{'autoLogout': 4, 'accountID': 'abcde'}
{
"autoLogout": 4,
"accountID": "abcde",
"someOtherField": 1234
}
Output in console:
{'autoLogout': 4, 'accountID': 'abcde', 'someOtherField': 1234}
{
"autoLogout": 4,
}
Output in console:
{'autoLogout': 4}
{
"autoLogout": 4,
"someOtherField": 1234
}
Output in console:
{'autoLogout': 4, 'someOtherField': 1234}
400 BAD REQUEST
{
"accountID": "abcde"
}
Response body:
{
"errors": {
"autoLogout": "'autoLogout' is a required property"
},
"message": "Input payload validation failed"
}
{
"accountID": "abcde",
"someOtherField": 1234
}
Response body:
{
"errors": {
"autoLogout": "'autoLogout' is a required property"
},
"message": "Input payload validation failed"
}
Does that clarify the behaviour? :smile:
If you wish to filter the payload after it has been validated by api.expect you could use marshal
within the body of the method:
from flask_restplus import marshal
...
def put(self, account_id):
filtered_input = marshal(request.json, account_settings_model)
print(filtered_input)
Payload:
{
"autoLogout": 4,
"accountID": "abcde",
"someOtherField": 1234
}
Output on console:
{'autoLogout': 4, 'accountID': 'abcde'}
@SteadBytes Incredible, thanks for the detailed explanation.
Actually, marshal
was exactly what I needed, I missed the fact that it can be used that way.
Thanks!
Great, glad to help! :smile: I'm going to close this issue now :+1:
@SteadBytes marshal does not work when model is defined with json schema. Any way to make it work?
Can you give a code example please? There are currently some unresolved issues going on around JSON schema models but without some code i can't say whether they're affecting you 😊
Sure, consider the following modal:
def field(type, **kwargs):
return {"type": type, **kwargs}
account_model = NS.schema_model('account',
{
"type": "object",
"required": ["name", "email", "userId"],
"properties": {
"name": field(
'string',
description="The name of the admin user for this account",
minLength=1,
example="Balpreet"
),
"email": field(
'string',
description="The email of the admin user for this account",
minLength=1,
example="balpreet.s@tyroo.com"
),
"userId": field(
'string',
description="The userId of the admin user for this account",
minLength=1,
example="2a714f61-fca2-4887-908c-03a9f75c0877"
)
}
}
)
Now, when i do, data = marshal(request.json, account_model)
i get the following error:
File "/Users/knowbalpreet/Desktop/work/main/spector.api/app/Modules/Account/controller.py", line 36, in post
return service.add_account(data=marshal(post_data, account))
File "/Users/knowbalpreet/Desktop/work/main/spector.api/venv3/lib/python3.7/site-packages/flask_restplus/marshalling.py", line 58, in marshal
out, has_wildcards = _marshal(data, fields, envelope, skip_none, mask, ordered)
File "/Users/knowbalpreet/Desktop/work/main/spector.api/venv3/lib/python3.7/site-packages/flask_restplus/marshalling.py", line 180, in _marshal
for k, v in iteritems(fields)
File "/Users/knowbalpreet/Desktop/work/main/spector.api/venv3/lib/python3.7/site-packages/six.py", line 587, in iteritems
return iter(d.items(**kw))
AttributeError: 'SchemaModel' object has no attribute 'items'
Maybe I misunderstand, but it seems to me that @api.expect should filter fields not in the model or throw an error: https://flask-restplus.readthedocs.io/en/stable/swagger.html#the-api-expect-decorator
The current behavior is that it doesn't do that. If it shouldn't block those fields by design, what other way do I have to not let extraneous fields into my business logic?
Currently I'm manually filtering
request.json
according to the model in expect().