marshmallow-sqlalchemy icon indicating copy to clipboard operation
marshmallow-sqlalchemy copied to clipboard

validation issue with Enum column

Open bitfusion-brian opened this issue 8 years ago • 2 comments

I use the serializer.load() function to validate my data and instantiate the SQLAlchemy object. If I try to set the Enum field as an Integer and load, then it throws:

venv/lib/python2.7/site-packages/marshmallow_sqlalchemy/schema.py:186: in load
    ret = super(ModelSchema, self).load(data, *args, **kwargs)
venv/lib/python2.7/site-packages/marshmallow/schema.py:568: in load
    result, errors = self._do_load(data, many, partial=partial, postprocess=True)
venv/lib/python2.7/site-packages/marshmallow/schema.py:648: in _do_load
    index_errors=self.opts.index_errors,
venv/lib/python2.7/site-packages/marshmallow/marshalling.py:295: in deserialize
    index=(index if index_errors else None)
venv/lib/python2.7/site-packages/marshmallow/marshalling.py:68: in call_and_store
    value = getter_func(data)
venv/lib/python2.7/site-packages/marshmallow/marshalling.py:288: in <lambda>
    data
venv/lib/python2.7/site-packages/marshmallow/fields.py:266: in deserialize
    self._validate(output)
venv/lib/python2.7/site-packages/marshmallow/fields.py:196: in _validate
    r = validator(value)
_ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _

self = <Length(min=None, max=9, equal=None, error=None)>, value = 123

    def __call__(self, value):
>       length = len(value)
E       TypeError: object of type 'int' has no len()

I assume this should instead just return the normal error instead of throwing an exception.

Here is my model class. I use the Node.create() method to to the validation/instantiation:

from marshmallow_sqlalchemy import ModelSchema

# SQLAlchemy session provider
from app import db

class Node(db.Model):
  id = db.Column(db.Integer, primary_key=True)
  state = db.Column(db.Enum('allocated', 'free', 'error'), default='free')

  @staticmethod
  def create(data):
    return NodeSerializer.load(data, session=db.session)


class NodeSchema(ModelSchema):
  class Meta:
    model = Node


NodeSerializer = NodeSchema()

Example failing code using above model class

data = {'state': 123}
node, err = Node.create(data) # Raises TypeError

bitfusion-brian avatar Jan 25 '17 21:01 bitfusion-brian

Did you find a solution for this?

jonasschalck avatar Oct 19 '21 08:10 jonasschalck

I ran into this issue as well. If you want to serialize by name, I used:

import enum

from flask import Flask
from flask_marshmallow import Marshmallow
from flask_sqlalchemy import SQLAlchemy
from marshmallow_sqlalchemy import ModelConverter as _ModelConverter
import sqlalchemy as sa
from sqlalchemy.dialects import mysql

app = Flask(__name__)

db = SQLAlchemy(app)
ma = Marshmallow(app)

class Enum(ma.Field):
    default_error_messages = {
        'invalid_type': 'Invalid type for value.',
        'one_of': 'Must be one of: {choices}.'}

    def __init__(self, *args, **kwargs):
        super().__init__(*args, **kwargs)

        self.enum = self.metadata.get('enum', None)

        if not self.enum:
            raise ValueError('Enum type not set.')

        if not issubclass(self.enum, enum.Enum):
            raise TypeError('Invalid type for enum.')

    def _deserialize(self, value, *args, **kwargs):
        return self._validated(value)

    def _serialize(self, value, *args, **kwargs):
        if not isinstance(value, self.enum):
            raise self.make_error('invalid_type')

        return value.name

    def _validate(self, value, *args, **kwargs):
        if not isinstance(value, self.num):
            super()._validate(value, *args, **kwargs)
        else:
            super()._validate(value.name, *args, **kwargs)

    def _validated(self, value):
        if value is None:
            return None

        if isinstance(value, self.enum):
            return value

        try:
            return getattr(self.enum, value)
        except AttributeError:
            choices = ', '.join(value.name for value in self.enum)

            raise self.make_error('one_of', choices=choices)

class ModelConverter(_ModelConverter):
    SQLA_TYPE_MAPPING = dict(
        list(_ModelConverter.SQLA_TYPE_MAPPING.items()) +
        [(sa.Enum, Enum), (mysql.ENUM, Enum)])

class PetType(enum.Enum):
    bird = 1
    cat = 2
    dog = 3

class Pet(db.model):
    id = db.Column(db.Integer, primary_key=True)
    type = db.Column(db.Enum(PetType), nullable=False)

class PetSchema(ma.SQLAlchemyAutoSchema):
    class Meta:
        model_converter = ModelConverter

    type = ma.auto_field(enum=PetType)

The largest issue was with enum.Enum not being serializable by default, SQLAlchemy serializing by name, and Marshmallow serializing by value. I wanted my enums to serialize by name and ended up with the above. If you want to serialize by value, you'll need to make some modifications.

uhnomoli avatar May 29 '22 20:05 uhnomoli