使用Marshmallow验证嵌套字段列表时遇类型错误的解决方法
问题根源
你发送请求时用json.dumps()把hyperparams序列化成了JSON字符串,但Marshmallow的fields.List(fields.Nested(HyperParameterSchema))期望接收的是Python列表对象。
当使用requests.post发送包含files的请求时,请求会自动以multipart/form-data格式发送,此时data字典里的所有值都会被当作字符串处理。Marshmallow接收到hyperparams的JSON字符串后,会把它当成列表的第一个元素,自然触发"Invalid type."的验证错误——因为它期望的是列表里每个元素都是符合HyperParameterSchema的字典,而不是一个字符串。
解决方案
方案1:后端添加预加载钩子解析JSON字符串
在LearnerRequestSchema中用pre_load装饰器,把接收到的JSON字符串转成Python列表后再验证:
from marshmallow import Schema, fields, pre_load, ValidationError import json class HyperParameterSchema(Schema): name = fields.Str(required=True) data_type = fields.Str(required=True) default_value = fields.Str(required=True) class LearnerRequestSchema(Schema): files = fields.Raw(type="file") name = fields.Str(required=True) description = fields.Str(required=True) supervision_scope = fields.Enum(SupervisionScope, by_value=True, required=True) task_type = fields.Enum(TaskType, by_value=True, required=True) tool = fields.Enum(Tool, by_value=True, required=True) learner_family_id = fields.Int(required=True) hyperparams = fields.List(fields.Nested(HyperParameterSchema)) @pre_load def parse_hyperparams(self, data, **kwargs): hyperparam_str = data.get('hyperparams') if isinstance(hyperparam_str, str): try: data['hyperparams'] = json.loads(hyperparam_str) except json.JSONDecodeError: raise ValidationError("hyperparams字段格式无效,需为合法JSON字符串") return data
这个方案不需要修改请求端代码,直接在后端完成格式转换,适合现有请求逻辑不变的场景。
方案2:请求端按multipart格式传递嵌套列表
修改请求代码,把嵌套列表拆成符合multipart/form-data规范的字段命名(类似hyperparams[index][key]的格式),Flask会自动把这些字段解析成嵌套字典的列表:
response = requests.post( url="http://localhost:8080/api/learners", data={ "name": "some_name", "description": "some_description", "supervision_scope": "supervised", "task_type": "classification", "tool": "scikit-learn", "learner_family_id": 1, # 按multipart规范传递嵌套列表 "hyperparams[0][name]": "alpha", "hyperparams[0][data_type]": "float", "hyperparams[0][default_value]": "0.1", "hyperparams[1][name]": "l1_ratio", "hyperparams[1][data_type]": "float", "hyperparams[1][default_value]": "0.1", }, files={ "file": open("path_to_file", "rb") }, headers=headers, )
这个方案更符合multipart/form-data的原生数据格式,后端不需要额外解析逻辑,验证流程更规范。
补充说明
你用fields.Raw()能绕过错误,是因为Raw字段不做任何类型校验,直接接收原始字符串值,但这样就丢失了Marshmallow自动验证嵌套结构的能力,需要手动解析和校验,所以不推荐长期使用。
内容的提问来源于stack exchange,提问作者Vinicius Kreischer

