Search code examples
jsonpython-3.xflaskenumsflask-sqlalchemy

Unable to get Enum Value on my response JSON


I am building flask project for my learning now, now I have simple schema for Employees like this (Didn't added all column names):

class GenderChoices(enum.Enum):
    M = 'M'
    F = 'F'

    def __str__(self):
        return self.name

    @staticmethod
    def from_string(s):
        try:
            return GenderChoices[s]
        except KeyError:
            raise ValueError()


class Employees(db.Model):
    ___tablename___ = "employees"

    emp_no = db.Column(db.Integer, primary_key=True)
    gender = db.Column(db.Enum(GenderChoices, values_callable=lambda x: [str(member.value)
                                                                         for member in GenderChoices]))

Basic Arguments on resources side, I am passing like that:

parser.add_argument('gender',
                    type=GenderChoices.from_string,
                    help='This field cannot be blank',
                    required=True,
                    choices=list(GenderChoices))

As I am showing my response like this:

return {
            'message': 'New Employee Has been created',
            'Employee_Details': {
                'employee_id': new_id,
                'gender': data['gender']
            }
        }, 201

While running my script to add Employees runs fine and it stores data on my database as well, but on showing my result on postman, I am getting following error:

TypeError: Object of type GenderChoices is not JSON serializable

What seems to be problem.


Solution

  • Flask-RESTful

    If you are using Flask-RESTful, you can force the conversion to a string by using the @marshal_with decorator and specifying a field.String.

    from flask import (
        Flask, 
        render_template, 
        request
    )
    from flask_restful import (
        Api, 
        Resource, 
        fields, 
        marshal_with, 
        reqparse
    )
    from flask_sqlalchemy import SQLAlchemy
    import enum 
    
    
    class GenderChoices(enum.Enum):
        M = 'M'
        F = 'F'
    
        def __str__(self):
            return self.name
    
        @staticmethod
        def from_string(s):
            try:
                return GenderChoices[s]
            except KeyError:
                raise ValueError()
    
    app = Flask(__name__)
    app.config.from_mapping(
        SECRET_KEY='your secret here', 
        SQLALCHEMY_DATABASE_URI='sqlite:///demo.db', 
    )
    db = SQLAlchemy(app)
    api = Api(app)
    
    parser = reqparse.RequestParser()
    parser.add_argument('gender',
        type=GenderChoices.from_string,
        help='This field cannot be blank',
        required=True,
        choices=list(GenderChoices)
    )
    
    class Employee(db.Model):
        __tablename__ = "employees"
        id = db.Column(db.Integer, primary_key=True)
        gender = db.Column(db.Enum(GenderChoices, values_callable=lambda x: [str(member.value) for member in GenderChoices]))
    
    with app.app_context():
        db.drop_all()
        db.create_all()
    
    class EmployeeList(Resource):
        @marshal_with({
            'message': fields.String,
            'Employee_Details': fields.Nested({
                'employee_id': fields.Integer, 
                'gender': fields.String 
            })
        })
        def post(self):
            args = parser.parse_args()
            employee = Employee(**args)
            db.session.add(employee)
            db.session.commit()
            return {
                'message': 'New Employee Has been created',
                'Employee_Details': {
                    'employee_id': employee.id,
                    'gender': employee.gender
                }
            }, 201
    
    api.add_resource(EmployeeList, '/employees')
    
    Flask

    When using pure flask you can use the following variants.

    On the one hand, you can use a StrEnum whose values can be converted to JSON without any action.

    class GenderChoices(enum.StrEnum):
        M = 'M'
        F = 'F'
    

    If you want to use an alternative method, you can create a custom JSONProvider and use that in your application. The following example extends the DefaultJSONProvider.

    from flask.json.provider import DefaultJSONProvider
    
    class CustomJSONProvider(DefaultJSONProvider):
        @staticmethod
        def default(o):
            if isinstance(o, enum.Enum):
                return o.name
            return DefaultJSONProvider.default(o)
    
    
    app = Flask(__name__)
    app.json_provider_class = CustomJSONProvider
    app.json = CustomJSONProvider(app)