Source code for backend.api.model_serializer
from flask_sqlalchemy.model import camel_to_snake_case
from marshmallow.exceptions import ValidationError
from backend.extensions.marshmallow import ma
from .constants import READ_ONLY_FIELDS
from .utils import to_camel_case
[docs]class ModelSerializer(ma.ModelSchema):
"""
Base class for database model serializers. This is pretty much a stock
:class:`flask_marshmallow.sqla.ModelSchema`: it will automatically create
fields from the attached database Model, the only difference being that it
will automatically dump to (and load from) the camel-cased variants of the
field names.
For example::
from backend.api import ModelSerializer
from backend.security.models import Role
class RoleSerializer(ModelSerializer):
class Meta:
model = Role
Is roughly equivalent to::
from marshmallow import Schema, fields
class RoleSerializer(Schema):
id = fields.Integer()
name = fields.String()
description = fields.String()
created_at = fields.DateTime(dump_to='createdAt',
load_from='createdAt')
updated_at = fields.DateTime(dump_to='updatedAt',
load_from='updatedAt')
Obviously you probably shouldn't be loading `created_at` or `updated_at`
from JSON; it's just an example to show the automatic snake-to-camelcase
field naming conversion.
"""
def is_create(self):
"""Check if we're creating a new object. Note that this context flag
must be set from the outside, ie when the class gets instantiated.
"""
return self.context.get('is_create', False)
def handle_error(self, error, data):
"""Customize the error messages for required/not-null validators with
dynamically generated field names. This is definitely a little hacky
(it mutates state, uses hardcoded strings), but unsure how better to do it
"""
required_messages = ('Missing data for required field.',
'Field may not be null.')
for field_name in error.field_names:
for i, msg in enumerate(error.messages[field_name]):
if msg in required_messages:
label = camel_to_snake_case(field_name).replace('_', ' ').title()
error.messages[field_name][i] = f'{label} is required.'
def _update_fields(self, obj=None, many=False):
"""Overridden to automatically convert snake-cased field names to
camel-cased (when dumping) and to load camel-cased field names back
to their snake-cased counterparts
"""
fields = super()._update_fields(obj, many)
new_fields = self.dict_class()
for name, field in fields.items():
if '_' in name and field.dump_to is None:
camel_cased_name = to_camel_case(name)
field.dump_to = camel_cased_name
field.load_from = camel_cased_name
new_fields[name] = field
# validate id
if 'id' in new_fields:
new_fields['id'].validators = [self.validate_id]
# set read-only fields
for name in READ_ONLY_FIELDS:
if name in new_fields:
new_fields[name].dump_only = True
self.fields = new_fields
return new_fields
def validate_id(self, id):
if self.is_create() or int(id) == int(self.instance.id):
return
raise ValidationError('ids do not match')