Skip to content

Commit 2d30538

Browse files
committed
add custom function and method fields
1 parent 9b9a1e1 commit 2d30538

File tree

2 files changed

+270
-0
lines changed

2 files changed

+270
-0
lines changed

README.md

Lines changed: 229 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,9 @@
44

55
- Use the marshmallow library to deserialize an input dictionary to a Python
66
class instance.
7+
- Filter fields as `load_only` or `dump_only`.
8+
- Define custom `function` and `method` fields to compute values during
9+
serialization.
710

811
---
912

@@ -399,6 +402,176 @@ $ python lib/load_only_dump_only.py
399402
'{"name": "Lua", "created_at": "2023-11-15T07:30:30.660156"}'
400403
```
401404

405+
---
406+
407+
---
408+
409+
## Custom Fields
410+
411+
You can create a custom field beyond the builtin field types. This is often done
412+
during serialization to compute a value from other fields. We will look at two
413+
options for creating a custom field (1) defining a `function` field, and (2)
414+
defining a `method` field.
415+
416+
Consider the code in `lib/custom_field.py`. A cat has fields `name`, `dob`, and
417+
`favorite_toys`.
418+
419+
```py
420+
# lib/custom_field.py
421+
422+
from marshmallow import Schema, fields, post_load
423+
from datetime import date
424+
from pprint import pprint
425+
426+
# model
427+
428+
class Cat:
429+
def __init__(self, name, dob, favorite_toys = []):
430+
self.name = name
431+
self.dob = dob
432+
self.favorite_toys = favorite_toys
433+
434+
# schema
435+
436+
class CatSchema(Schema):
437+
name = fields.Str(required=True, error_messages={"required": "Name is required."})
438+
dob = fields.Date(format="%Y-%m-%d")
439+
favorite_toys = fields.List(fields.Str())
440+
441+
@post_load
442+
def make_cat(self, data, **kwargs):
443+
return Cat(**data)
444+
445+
schema = CatSchema()
446+
447+
#deserialize
448+
cat_1 = schema.load({"name": "Meowie", "dob": "2020-11-28", "favorite_toys": ["ball", "squeaky mouse"]})
449+
cat_2 = schema.load({"name": "Whiskers", "dob": "2015-4-15", "favorite_toys": []})
450+
451+
#serialize
452+
pprint(schema.dump(cat_1))
453+
# => {'age': 2,
454+
# => 'dob': '2020-11-28',
455+
# => 'favorite_toys': ['ball', 'squeaky mouse']}
456+
457+
pprint(schema.dump(cat_2))
458+
# => {'age': 8,
459+
# => 'dob': '2015-04-15',
460+
# => 'favorite_toys': []}
461+
```
462+
463+
Running the code produces the expected output:
464+
465+
```console
466+
{'dob': '2020-11-28',
467+
'favorite_toys': ['ball', 'squeaky mouse'],
468+
'name': 'Meowie'}
469+
{'dob': '2015-04-15', 'favorite_toys': [], 'name': 'Whiskers'}
470+
```
471+
472+
Suppose we would like to include in the serialized output two more fields:
473+
474+
- `likes_toys` : a boolean that is true if the list of favorite toys is not
475+
empty
476+
- `age` : an integer calculated using the date of birth and the current date.
477+
478+
These fields will not be included during loading, since they can be calculated
479+
from the other fields. While we could update the model class to calculate them,
480+
we can also just update the schema to compute them during serialization.
481+
482+
We can use define `likes_toys` using the class `fields.Function` as shown below.
483+
We pass a callable from which to compute the value. The function must take a
484+
single argument `obj` which is the object to be serialized.
485+
486+
```py
487+
likes_toys = fields.Function(lambda obj : len(obj.favorite_toys) > 0, dump_only = True)
488+
```
489+
490+
The calculation for the `age` field doesn't necessarily work as a simple lambda
491+
expression, so we'll implement that field using the class `fields.Method` as
492+
shown:
493+
494+
```py
495+
age = fields.Method("calculate_age", dump_only = True)
496+
497+
def calculate_age(self, obj):
498+
today = date.today()
499+
return today.year - obj.dob.year - ((today.month, today.day) < (obj.dob.month, obj.dob.day))
500+
501+
```
502+
503+
Update the code to add the `likes_toys` function and `age` method fields to the
504+
schema:
505+
506+
```py
507+
# lib/custom_field.py
508+
509+
from marshmallow import Schema, fields, post_load
510+
from datetime import date
511+
from pprint import pprint
512+
513+
# model
514+
515+
class Cat:
516+
def __init__(self, name, dob, favorite_toys = []):
517+
self.name = name
518+
self.dob = dob
519+
self.favorite_toys = favorite_toys
520+
521+
# schema
522+
523+
class CatSchema(Schema):
524+
name = fields.Str(required=True, error_messages={"required": "Name is required."})
525+
dob = fields.Date(format="%Y-%m-%d")
526+
favorite_toys = fields.List(fields.Str())
527+
likes_toys = fields.Function(lambda obj : len(obj.favorite_toys) > 0, dump_only = True)
528+
age = fields.Method("calculate_age", dump_only = True)
529+
530+
def calculate_age(self, obj):
531+
today = date.today()
532+
return today.year - obj.dob.year - ((today.month, today.day) < (obj.dob.month, obj.dob.day))
533+
534+
@post_load
535+
def make_cat(self, data, **kwargs):
536+
return Cat(**data)
537+
538+
schema = CatSchema()
539+
540+
#deserialize
541+
cat_1 = schema.load({"name": "Meowie", "dob": "2020-11-28", "favorite_toys": ["ball", "squeaky mouse"]})
542+
cat_2 = schema.load({"name": "Whiskers", "dob": "2015-4-15", "favorite_toys": []})
543+
544+
#serialize
545+
pprint(schema.dump(cat_1))
546+
# => {'age': 2,
547+
# => 'dob': '2020-11-28',
548+
# => 'favorite_toys': ['ball', 'squeaky mouse'],
549+
# => 'likes_toys': True,
550+
# => 'name': 'Meowie'}
551+
552+
pprint(schema.dump(cat_2))
553+
# => {'age': 8,
554+
# => 'dob': '2015-04-15',
555+
# => 'favorite_toys': [],
556+
# => 'likes_toys': False,
557+
# => 'name': 'Whiskers'}
558+
```
559+
560+
Now we see `age` and `likes_toys` included in the serialized output:
561+
562+
```console
563+
{'age': 2,
564+
'dob': '2020-11-28',
565+
'favorite_toys': ['ball', 'squeaky mouse'],
566+
'likes_toys': True,
567+
'name': 'Meowie'}
568+
{'age': 8,
569+
'dob': '2015-04-15',
570+
'favorite_toys': [],
571+
'likes_toys': False,
572+
'name': 'Whiskers'}
573+
```
574+
402575
## Conclusion
403576

404577
This lesson has covered quite a few concepts involving deserialization:
@@ -417,6 +590,8 @@ This lesson has covered quite a few concepts involving deserialization:
417590
indicates the field should be serialized but not used during deserialization.
418591
- By default, a field is defined as `load_only=False` and `dump_only=False`,
419592
meaning it will be used for both serialization and deserialization.
593+
- Custom fields can be created using `fields.Function` and `fields.Method`
594+
types.
420595

421596
## Solution Code
422597

@@ -566,6 +741,60 @@ pprint(user_schema.dumps(user)) # password is load_only
566741
# => '{"name": "Lua", "created_at": "2023-11-11T10:56:55.898190"}'
567742
```
568743

744+
```py
745+
# lib/custom_field.py
746+
747+
from marshmallow import Schema, fields, post_load
748+
from datetime import date
749+
from pprint import pprint
750+
751+
# model
752+
753+
class Cat:
754+
def __init__(self, name, dob, favorite_toys = []):
755+
self.name = name
756+
self.dob = dob
757+
self.favorite_toys = favorite_toys
758+
759+
# schema
760+
761+
class CatSchema(Schema):
762+
name = fields.Str(required=True, error_messages={"required": "Name is required."})
763+
dob = fields.Date(format="%Y-%m-%d")
764+
favorite_toys = fields.List(fields.Str())
765+
likes_toys = fields.Function(lambda obj : len(obj.favorite_toys) > 0, dump_only = True)
766+
age = fields.Method("calculate_age", dump_only = True)
767+
768+
def calculate_age(self, obj):
769+
today = date.today()
770+
return today.year - obj.dob.year - ((today.month, today.day) < (obj.dob.month, obj.dob.day))
771+
772+
@post_load
773+
def make_cat(self, data, **kwargs):
774+
return Cat(**data)
775+
776+
schema = CatSchema()
777+
778+
#deserialize
779+
cat_1 = schema.load({"name": "Meowie", "dob": "2020-11-28", "favorite_toys": ["ball", "squeaky mouse"]})
780+
cat_2 = schema.load({"name": "Whiskers", "dob": "2015-4-15", "favorite_toys": []})
781+
782+
#serialize
783+
pprint(schema.dump(cat_1))
784+
# => {'age': 2,
785+
# => 'dob': '2020-11-28',
786+
# => 'favorite_toys': ['ball', 'squeaky mouse'],
787+
# => 'likes_toys': True,
788+
# => 'name': 'Meowie'}
789+
790+
pprint(schema.dump(cat_2))
791+
# => {'age': 8,
792+
# => 'dob': '2015-04-15',
793+
# => 'favorite_toys': [],
794+
# => 'likes_toys': False,
795+
# => 'name': 'Whiskers'}
796+
```
797+
569798
---
570799

571800
## Resources

lib/custom_field.py

Lines changed: 41 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,41 @@
1+
# lib/custom_field.py
2+
3+
from marshmallow import Schema, fields, post_load
4+
from datetime import date
5+
from pprint import pprint
6+
7+
# model
8+
9+
class Cat:
10+
def __init__(self, name, dob, favorite_toys = []):
11+
self.name = name
12+
self.dob = dob
13+
self.favorite_toys = favorite_toys
14+
15+
# schema
16+
17+
class CatSchema(Schema):
18+
name = fields.Str(required=True, error_messages={"required": "Name is required."})
19+
dob = fields.Date(format="%Y-%m-%d")
20+
favorite_toys = fields.List(fields.Str())
21+
22+
@post_load
23+
def make_cat(self, data, **kwargs):
24+
return Cat(**data)
25+
26+
schema = CatSchema()
27+
28+
#deserialize
29+
cat_1 = schema.load({"name": "Meowie", "dob": "2020-11-28", "favorite_toys": ["ball", "squeaky mouse"]})
30+
cat_2 = schema.load({"name": "Whiskers", "dob": "2015-4-15", "favorite_toys": []})
31+
32+
#serialize
33+
pprint(schema.dump(cat_1))
34+
# => {'age': 2,
35+
# => 'dob': '2020-11-28',
36+
# => 'favorite_toys': ['ball', 'squeaky mouse']}
37+
38+
pprint(schema.dump(cat_2))
39+
# => {'age': 8,
40+
# => 'dob': '2015-04-15',
41+
# => 'favorite_toys': []}

0 commit comments

Comments
 (0)