flaskmarshallingflask-restx

Allow null for Flask restx's expect


I have setup a Flask project using the flask restx to help me validate the input JSON payload. Note: I am also using restx's Namespace for other things too, so I have ns instead of blueprint

I have something like this:

@ns.route('/event')
class EventResource(Resource):
    @ns.expect(event_model, validate=True)
    def post(self):
        validated_data = ns.payload  # Use the payload directly from the request
        logger.info(f"Validated event data: {validated_data}")
        ...

For the model, I have a nested model like this:

event_department_model = ns.model('EventDepartment', {
    'identifier': fields.String(required=True, description='Department identifier'),
    'name': fields.String(required=True, description='Department name')
})

event_model = ns.model('Event', {
    ...
    'department': fields.Nested(event_department_model, required=False, allow_null=True, skip_none=True, nullable=True, description='Department information'),
    ...
}

I want department in the input JSON payload to accept null. So in event_model, I have already tried required=False, allow_null=True, nullable=True, skip_none=True and different combinations of these options.

However, no matter what combinations I have tried, I am getting the same error:

{
    "errors": {
        "department": "None is not of type 'object'"
    },
    "message": "Input payload validation failed"
}

Did some searching online but I couldn't find anything similar to my issue. My online search result tells me that it is not liking the null value in the JSON because it is expecting an object... even though I have already put down options such as required=False, allow_null=True. I see people usually get this resolved by setting something like allow_null or skip_none. I am not sure what went wrong.


Solution

  • Tracing the responses from your GitHub Issue to flask-restx, it looks like it is a fundamental limitation of how schemas are handled in flask-restx. However, a workaround was proposed to flask-restplus (which flask-restx is a fork of), and that same approach can be modified to fit the current flask-restx . The approach is to create a subclass of Nested (this could be simplified for the use-case, this was written based on matching the base fields.Nested's schema method):

    class NullableNested(fields.Nested):
        def schema(self):
            schema = super().schema()
            if self.as_list:
                ref = schema['items'].pop('$ref')
                schema['items']['anyOf'] = [
                    {'$ref': ref},
                    {'type': 'null'}
                ]
                return schema
            elif any(schema.values()):
                all_of = schema.pop('allOf')
                return {
                    'anyOf': [
                        {'allOf': all_of},
                        {'type': 'null'}
                    ]
                }
            else:
                ref = schema.pop('$ref')
                return {
                    'anyOf': [
                        {'$ref': ref},
                        {'type': 'null'}
                    ]
                }
    

    In total, for the minimal reproducible example:

    from flask import Flask
    from flask_restx import Api, Resource, fields
    
    app = Flask(__name__)
    api = Api(app, doc='/docs')
    ns = api.namespace('test', description='Test namespace')
    
    class NullableNested(fields.Nested):
        def schema(self):
            schema = super().schema()
            if self.as_list:
                ref = schema['items'].pop('$ref')
                schema['items']['anyOf'] = [
                    {'$ref': ref},
                    {'type': 'null'}
                ]
                return schema
            elif any(schema.values()):
                all_of = schema.pop('allOf')
                return {
                    'anyOf': [
                        {'allOf': all_of},
                        {'type': 'null'}
                    ]
                }
            else:
                ref = schema.pop('$ref')
                return {
                    'anyOf': [
                        {'$ref': ref},
                        {'type': 'null'}
                    ]
                }
    
    
    # Define the nested model
    event_department_model = ns.model('EventDepartment', {
        'identifier': fields.String(required=True, description='Department identifier'),
        'name': fields.String(required=True, description='Department name')
    })
    
    # Define the main event model
    event_model = ns.model('Event', {
        'event_id': fields.String(required=True, description='Event ID'),
        'department': NullableNested(
            event_department_model,
            required=False,
            description='Department info (can be null)',
            allow_null=True,  # Tried this
            skip_none=True,  # And this
            nullable=True  # And this too
        )
    })
    
    
    @ns.route('/event')
    class EventResource(Resource):
        @ns.expect(event_model, validate=True)
        def post(self):
            validated_data = ns.payload
            return {'received': validated_data}, 200
    
    
    api.add_namespace(ns)
    
    if __name__ == '__main__':
        app.run(debug=True)
    

    Then if you run:

    curl -X POST 'http://127.0.0.1:5000/test/event' --data '{
      "event_id": "abc123",
      "department": null
    }'  -H 'Content-Type: application/json'
    

    The output is the expected:

    {
        "received": {
            "event_id": "abc123",
            "department": null
        }
    }