简体   繁体   中英

Flask Python Model Validation

Coming from a php background, I am learning python through Flask. I have used WTForms for the client, and this handles validation nicely.

However, one of the things that I would like to use flask for is a public API, in which case I would like all validation to be run on my models. I thought that SQLAlchemy would include validation functionality, but this doesn't seem to be the case.

I have come across Colander , which looks nice, but I am kinda surprised that there are not more ubiquitous validation libraries. And even more surprised that SQLAlchemy doesn't do this natively.

What are the options here? Perhaps I am missing something, but how can I easily validate model data?

Have you considered doing the validation in the Model layer...

This would allow you to have a perfectly DRY solution as validation would be automatically triggered whether the update source is data sent by the user, or whether it is a component of your application which is updating the model as part of an indirect update. In short, you could also reuse this solution in your front-end with WTForms, and have only one place where you do your validation for both your API and your front-end.

See this answer for more pros of doing the validation in the model.


...using tools provided by SQLAlchemy?

1. The validates() decorator for simple validation:

Using this decorator is pretty straightforward: just apply it to the fields you want to validate:

from sqlalchemy.orm import validates

class EmailAddress(Base):
    __tablename__ = 'address'

    id = Column(Integer, primary_key=True)
    email = Column(String)

    @validates('email')
    def validate_email(self, key, address):
        assert '@' in address
        return address

2. ORM Events for complex business rules:

You can use attribute events to perform complex validation directly when one of the attributes of an instance of a model is changed. The advantage of using attribute events is that you are guaranteed that the data in the session (the objects in-memory) are in a validated state.

Here is an example ( a simple one , but you should think complex rules here) from the docs :

def validate_phone(target, value, oldvalue, initiator):
    "Strip non-numeric characters from a phone number"

    return re.sub(r'(?![0-9])', '', value)

# setup listener on UserContact.phone attribute, instructing
# it to use the return value
listen(UserContact.phone, 'set', validate_phone, retval=True)

You could also use Mapper Events such as before_insert to postpone validation to the session.add() call, or even use Session Events to intercept commits... But you lose the integrity guarantee of the data in the session ...

I'm writing a library for this, called Flask-Inputs .

Similar to Colander, you define schemas and validate your inputs against them. Like @Sean Vieira's suggestion, it relies on WTForms for validation.

Internally, it converts all request input data to MultiDicts. Just like WTForms, you can define custom validators (one built-in custom validator is for request.json data, it uses jsonschema for validation).

Since it sounds like you're running validation on data posted to a public API, here's an example for API key and posted JSON validation.

from flask_inputs import Inputs
from flask_inputs.validators import JsonSchema

schema = {
    'type': 'object',
    'properties': {
        'name': {'type': 'string'}
    }
}

class ApiInputs(Inputs):
    headers = {
        'Authorization': [DataRequired(), valid_api_key]
    }
    json = [JsonSchema(schema=schema)]

Then in your route:

@app.route('/api/<version>/endpoint')
def endpoint():
    inputs = ApiInputs(request)

    if not inputs.validate():
        return jsonify(success=False, errors=inputs.errors)

The biggest benefits I've found (using it in production) is surfacing all errors in one place. Good validators covering all incoming data prevent a lot of unexpected/undefined behavior in production.

As long as the data coming in can be read in a Multi-Dict like format there is no reason why you can't still use WTForms for the validation (albeit, it is a little more awkward than using Colander).

So for a hypothetical API that produces and consumes JSON you might do something like this:

class MyDataStructure(Form):
    widget = TextField("Widget", validators=[Required()])
    quantity = IntegerField("Quantity", validators=[Required()])

@app.route("/api/v1/widgets", methods=["POST"])
def widgets():
    try:
        new_widget_info = json.loads(request.form.data)
    except KeyError:
        return jsonify(error="Must provide widget JSON in data param")
    except ValueError:
        return jsonify(error="Invalid JSON Provided")

    data = MyDataStructure(**new_widget_info)
    if not data.validate():
        return jsonify(error="Missing or invalid data",
                           error_details=data.errors)
    else:
        # Create a new widget

The technical post webpages of this site follow the CC BY-SA 4.0 protocol. If you need to reprint, please indicate the site URL or the original address.Any question please contact:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM