marshmallow扩展Schemas
- Pre-processing and Post-processing Methods
可以使用pre_load,post_load,pre_dump,post_dump装饰器,来对(反)序列化数据进行预处理和后处理操作
1 from marshmallow import Schema, fields, pre_load 2 3 class UserSchema(Schema): 4 name = fields.String() 5 slug = fields.String() 6 7 @pre_load 8 def slugify_name(self, in_data, **kwargs): 9 print(1111) 10 in_data["slug"] = in_data["slug"].lower().strip().replace(" ", "-") 11 return in_data 12 13 @post_load 14 def make_user(self, in_data, **kwargs): 15 print(2222) 16 return in_data 17 18 schema = UserSchema() 19 result = schema.load({"name": "Steve", "slug": "Steve Loria"}) 20 pprint(result) 21 22 11111 23 22222 24 {'name': 'Steve', 'slug': 'steve-loria'}
- Passing “many”
默认情况下,预处理和后处理方法同时只能接受一个对象/数据,如果要处理一个集合的数据,只需给装饰器方法传入pass_many=True即可
1 from marshmallow import Schema, fields, pre_load, post_load, post_dump 2 3 class BaseSchema(Schema): 4 __envelope__ = {"single": None, "many": None} 5 __model__ = User 6 7 def get_envelope_key(self, many): 8 key = self.__envelope__["many"] if many else self.__envelope__["single"] 9 assert key is not None, "Envelope key undefined" 10 return key 11 12 @pre_load(pass_many=True) 13 def unwrap_envelope(self, data, many, **kwargs): 14 key = self.get_envelope_key(many) 15 return data[key] 16 17 @post_dump(pass_many=True) 18 def wrap_with_envelope(self, data, many, **kwargs): 19 key = self.get_envelope_key(many) 20 return {key: data} 21 22 @post_load 23 def make_object(self, data, **kwargs): 24 return self.__model__(**data) 25 26 class UserSchema(BaseSchema): 27 __envelope__ = {"single": "user", "many": "users"} 28 __model__ = User 29 name = fields.Str() 30 email = fields.Email() 31 32 user_schema = UserSchema() 33 user = User("Mike", "mike@stones.com") 34 pprint(user_schema.dump(user)) 35 36 {'user': {'email': 'mike@stones.com', 'name': 'Mike'}} 37 38 users = [ 39 User("Keith", "keith@stones.com"), 40 User("Charlie", "charlie@stones.com"), 41 ] 42 users_data = user_schema.dump(users, many=True) 43 pprint(users_data) 44 45 {'users': [{'email': 'keith@stones.com', 'name': 'Keith'}, 46 {'email': 'charlie@stones.com', 'name': 'Charlie'}]} 47 48 pprint(user_schema.load(users_data, many=True)) 49 50 [<User(name='Keith')>, <User(name='Charlie')>]
- Raising Errors in Pre-/Post-processor Methods
预处理和后处理方法可能会触发ValidationError,默认情况下,错误会被保存在以“_schema”为键的字典中
1 from marshmallow import Schema, fields, ValidationError, pre_load 2 3 class BandSchema(Schema): 4 name = fields.Str() 5 6 @pre_load 7 def unwrap_envelope(self, data, **kwargs): 8 if "data" not in data: 9 raise ValidationError("Input data must have a 'data' key.") 10 return data["data"] 11 12 sch = BandSchema() 13 try: 14 sch.load({"name": "The Band"}) 15 except ValidationError as err: 16 print(err.messages) 17 18 {'_schema': ["Input data must have a 'data' key."]}
如果不想用“_schema”作为错误字典的键,只需往ValidationError传入第二个参数(自定义键)即可
1 from marshmallow import Schema, fields, ValidationError, pre_load 2 3 class BandSchema(Schema): 4 name = fields.Str() 5 6 @pre_load 7 def unwrap_envelope(self, data, **kwargs): 8 if "data" not in data: 9 raise ValidationError("Input data must have a 'data' key.", "_preprocessing") 10 return data["data"] 11 12 sch = BandSchema() 13 try: 14 sch.load({"name": "The Band"}) 15 except ValidationError as err: 16 print(err.messages) 17 18 {'_preprocessing': ["Input data must have a 'data' key."]}
- Pre-/Post-processor Invocation Order
反序列化处理调用顺序如下:
- @pre_load(pass_many=True) methods
- @pre_load(pass_many=False) methods
- load(in_data, many) (validation and deserialization)
- @post_load(pass_many=True) methods
- @post_load(pass_many=False) methods
序列化类似,只是带pass_many=True的处理方法会在带pass_many=False的处理方法之后被调用
- @pre_dump(pass_many=False) methods
- @pre_dump(pass_many=True) methods
- dump(obj, many) (serialization)
- @post_dump(pass_many=False) methods
- @post_dump(pass_many=True) methods
Warning:
你可能会在同一个schema上注册多个处理方法,如果出现多个同类型的处理装饰器,则schema无法保证调用顺序。
如果你需要保证调用顺序,你应该将它们放到同一个方法里。
1 from marshmallow import Schema, fields, pre_load 2 3 # YES 4 class MySchema(Schema): 5 field_a = fields.Field() 6 7 @pre_load 8 def preprocess(self, data, **kwargs): 9 step1_data = self.step1(data) 10 step2_data = self.step2(step1_data) 11 return step2_data 12 13 def step1(self, data): 14 do_step1(data) 15 16 # Depends on step1 17 def step2(self, data): 18 do_step2(data) 19 20 21 # NO 22 class MySchema(Schema): 23 field_a = fields.Field() 24 25 @pre_load 26 def step1(self, data, **kwargs): 27 do_step1(data) 28 29 # Depends on step1 30 @pre_load 31 def step2(self, data, **kwargs): 32 do_step2(data)
- Schema-level Validation
可以使用marshmallow.validates_schema装饰器来注册schema级别的验证方法
1 from marshmallow import Schema, fields, validates_schema, ValidationError 2 3 class NumberSchema(Schema): 4 field_a = fields.Integer() 5 field_b = fields.Integer() 6 7 @validates_schema 8 def validate_numbers(self, data, **kwargs): 9 if data["field_b"] >= data["field_a"]: 10 raise ValidationError("field_a must be greater than field_b") 11 12 schema = NumberSchema() 13 try: 14 schema.load({"field_a": 1, "field_b": 2}) 15 except ValidationError as err: 16 print(err.messages) 17 18 {'_schema': ['field_a must be greater than field_b']}
- Storing Errors on Specific Fields
当多个schema级别的验证器返回错误信息时,这些错误信息将在验证结束后被合并到一起返回
1 from marshmallow import Schema, fields, validates_schema, ValidationError 2 3 class NumberSchema(Schema): 4 field_a = fields.Integer() 5 field_b = fields.Integer() 6 field_c = fields.Integer() 7 field_d = fields.Integer() 8 9 @validates_schema 10 def validate_lower_bound(self, data, **kwargs): 11 errors = {} 12 if data["field_b"] <= data["field_a"]: 13 errors["field_b"] = ["field_b must be greater than field_a"] 14 if data["field_c"] <= data["field_a"]: 15 errors["field_c"] = ["field_c must be greater than field_a"] 16 if errors: 17 raise ValidationError(errors) 18 19 @validates_schema 20 def validate_upper_bound(self, data, **kwargs): 21 errors = {} 22 if data["field_b"] >= data["field_d"]: 23 errors["field_b"] = ["field_b must be lower than field_d"] 24 if data["field_c"] >= data["field_d"]: 25 errors["field_c"] = ["field_c must be lower than field_d"] 26 if errors: 27 raise ValidationError(errors) 28 29 schema = NumberSchema() 30 try: 31 schema.load({ 32 "field_a": 3, 33 "field_b": 2, 34 "field_c": 1, 35 "field_d": 0 36 }) 37 except ValidationError as err: 38 pprint(err.messages) 39 40 {'field_b': ['field_b must be greater than field_a', 41 'field_b must be lower than field_d'], 42 'field_c': ['field_c must be greater than field_a', 43 'field_c must be lower than field_d']}
- Using Original Input Data
如果想使用原始的未处理的输入数据,只需要在post_load装饰器上传入pass_original=True即可
1 from marshmallow import Schema, fields, post_load, ValidationError 2 3 class MySchema(Schema): 4 foo = fields.Integer() 5 bar = fields.Integer() 6 7 class Meta: 8 unknown = EXCLUDE 9 10 @post_load(pass_original=True) 11 def add_baz_to_bar(self, data, original_data, **kwargs): 12 baz = original_data.get("baz") 13 if baz: 14 data["bar"] = data["bar"] + baz 15 return data 16 17 schema = MySchema() 18 pprint(schema.load({"foo": 1, "bar": 2, "baz": 3})) 19 20 {'bar': 5, 'foo': 1}
- Custom Error Handling
自定义错误处理,可以通过重写handle_error方法来自定义一个错误处理方法,这个方法接受ValidationError和原始的反序列化的输入数据
1 import logging 2 from marshmallow import Schema, fields 3 4 class AppError(Exception): 5 pass 6 7 class UserSchema(Schema): 8 email = fields.Email() 9 10 def handle_error(self, error, data, **kwargs): 11 logging.error(error.messages) 12 raise AppError("An error occurred with input: {0}".format(data)) 13 14 schema = UserSchema() 15 schema.load({"email": "invalid-email"}) 16 17 AppError: An error occurred with input: {'email': 'invalid-email'}
- Custom “class Meta” Options
class Meta 设置是一种配置和修改schema行为的方式,查看API
可以通过子类SchemaOpts来自定义class Meta配置
- Example:Enveloping,Revisited
同样为上面的的例子添加一个序列化输出的封装。这次,使用封装的键来自定义class Meta的配置。
1 # Example outputs 2 { 3 'user': { 4 'name': 'Keith', 5 'email': 'keith@stones.com' 6 } 7 } 8 # List output 9 { 10 'users': [{'name': 'Keith'}, {'name': 'Mick'}] 11 }
首先,添加命名空间配置到一个自定义的设置类
1 from marshmallow import Schema, SchemaOpts 2 3 class NamespaceOpts(SchemaOpts): 4 """Same as the default class Meta options, but adds "name" and 5 "plural_name" options for enveloping. 6 """ 7 8 def __init__(self, meta, **kwargs): 9 SchemaOpts.__init__(self, meta, **kwargs) 10 self.name = getattr(meta, "name", None) 11 self.plural_name = getattr(meta, "plural_name", self.name)
然后用这个设置类创建一个自定义的schema
1 class NamespacedSchema(Schema): 2 OPTIONS_CLASS = NamespaceOpts 3 4 @pre_load(pass_many=True) 5 def unwrap_envelope(self, data, many, **kwargs): 6 key = self.opts.plural_name if many else self.opts.name 7 return data[key] 8 9 @post_dump(pass_many=True) 10 def wrap_with_envelope(self, data, many, **kwargs): 11 key = self.opts.plural_name if many else self.opts.name 12 return {key: data}
现在应用schemas就可以继承这个自定义的schema类了
1 class UserSchema(NamespacedSchema): 2 name = fields.String() 3 email = fields.Email() 4 5 class Meta: 6 name = "user" 7 plural_name = "users" 8 9 10 ser = UserSchema() 11 user = User("Keith", email="keith@stones.com") 12 result = ser.dump(user) 13 result # {"user": {"name": "Keith", "email": "keith@stones.com"}}