EDIT
So it is possible to validate collections using just marshmallow. You can use the pass_many
kwarg with pre_load
or post_load
methods. I did not have success with the pre_load
but got to work with post. The pass_many
kwarg will treat the input as a collection so then you can check the length of the collection after loaded. I use the many
kwarg so that I will only check the length if we are passing a collection of records rather than just an individual record
from marshmallow import Schema, fields, ValidationError, post_load
class Record(Schema):
id = fields.Integer(required=True)
name = fields.String(required=True)
status = fields.String(required=True)
@post_load(pass_many=True)
def check_length(self, data, many, **kwargs):
if many:
if len(data) < 1 or len(data) > 10:
raise ValidationError(message=['Record length should be greater than 1 and less than 10.'],
field_name='record')
EDIT TEST CASES
from unittest import TestCase
from marshmallow import ValidationError
from stack_marshmallow import Record
class TestStackSchemasNonNested(TestCase):
def test_empty_dict(self):
with self.assertRaises(ValidationError) as exc:
Record(many=True).load([])
self.assertEqual(exc.exception.messages['record'], ['Record length should be greater than 1 and less than 10.'])
def test_happy_path(self):
user_data = [{"id": "1", "name": "apple", "status": "OK"}, {"id": "2", "name": "apple", "status": 'OK'}]
data = Record(many=True).load(user_data)
self.assertEqual(len(data), 2)
def test_invalid_values_with_valid_values(self):
user_data = [{"id": "1", "name": "apple", "status": 'OK'}, {"id": "2"}]
with self.assertRaises(ValidationError) as exc:
Record(many=True).load(user_data)
self.assertEqual(exc.exception.messages[1]['name'], ['Missing data for required field.'])
self.assertEqual(exc.exception.messages[1]['status'], ['Missing data for required field.'])
def test_too_many(self):
user_data = [{"id": "1", "name": "apple", "status": "OK"},
{"id": "2", "name": "apple", "status": 'OK'},
{"id": "3", "name": "apple", "status": 'OK'},
{"id": "4", "name": "apple", "status": 'OK'},
{"id": "5", "name": "apple", "status": 'OK'},
{"id": "6", "name": "apple", "status": 'OK'},
{"id": "7", "name": "apple", "status": 'OK'},
{"id": "8", "name": "apple", "status": 'OK'},
{"id": "9", "name": "apple", "status": 'OK'},
{"id": "10", "name": "apple", "status": 'OK'},
{"id": "11", "name": "apple", "status": 'OK'},
]
with self.assertRaises(ValidationError) as exc:
Record(many=True).load(user_data)
self.assertEqual(exc.exception.messages['record'], ['Record length should be greater than 1 and less than 10.'])
EDIT SOURCES: https://marshmallow.readthedocs.io/en/stable/extending.html
You are very close. I added a little more complexity to record because I don't think you will just have one field or else I would just use a List of Integers. I added some unit tests as well so you can see how to test it.
from marshmallow import Schema, fields, validate
class Record(Schema):
id = fields.Integer(required=True)
name = fields.String(required=True)
status = fields.String(required=True)
class Records(Schema):
records = fields.List(
fields.Nested(Record),
required=True,
validate=validate.Length(min=1,max=10)
)
TEST CASES
from unittest import TestCase
from marshmallow import ValidationError
from stack_marshmallow import Records
class TestStackSchemas(TestCase):
def setUp(self):
self.schema = Records()
def test_empty_dict(self):
with self.assertRaises(ValidationError) as exc:
self.schema.load({})
self.assertEqual(exc.exception.messages['records'], ['Missing data for required field.'])
def test_empty_empty_list_in_dict(self):
with self.assertRaises(ValidationError) as exc:
self.schema.load({"records": []})
self.assertEqual(exc.exception.messages['records'], ['Length must be between 1 and 10.'])
def test_missing_fields_in_single_record(self):
with self.assertRaises(ValidationError) as exc:
self.schema.load({"records": [{"id": 1}]})
self.assertEqual(exc.exception.messages['records'][0]['name'], ['Missing data for required field.'])
self.assertEqual(exc.exception.messages['records'][0]['status'], ['Missing data for required field.'])
def test_list_too_long_and_invalid_records(self):
with self.assertRaises(ValidationError) as exc:
self.schema.load({"records":
[{"id": 1, "name": "stack", "status": "overflow"},
{"id": 2, "name": "stack", "status": "overflow"},
{"id": 3, "name": "stack", "status": "overflow"},
{"id": 4, "name": "stack", "status": "overflow"},
{"id": 5, "name": "stack", "status": "overflow"},
{"id": 6, "name": "stack", "status": "overflow"},
{"id": 7, "name": "stack", "status": "overflow"},
{"id": 8, "name": "stack", "status": "overflow"},
{"id": 9, "name": "stack", "status": "overflow"},
{"id": 10, "name": "stack", "status": "overflow"},
{"id": 11, "name": "stack", "status": "overflow"}]})
self.assertEqual(exc.exception.messages['records'], ['Length must be between 1 and 10.'])
Sources: https://marshmallow.readthedocs.io/en/stable/nesting.html and
https://marshmallow.readthedocs.io/en/stable/examples.html