marshmallow
marshmallow copied to clipboard
fields.Method result should be able to be wrapped into other field types
fields.Method is pretty much always used to return a certain type of value. It doesn't allow to choose options for the return value though.
For example, if one designes a field which should return a generic datetime and format it using a given format string, one is forced to use fields.Method which is linked to a schema method which returns a string(!), not a datetime. Because it is now impossible to provide a format string for the datetime anywhere except of the method itself.
In other words, something like this:
class MySchema(Schema):
created_at = fields.Datetime(method='_created_at', format='%Y-%m-%dT%H:%M:%S%z')
def _created_at(self, obj):
return obj.metadata().creation_time()
would be much more convenient and reusable than this:
class MySchema(Schema):
created_at = fields.Method('_formatted_created_at')
def _formatted_created_at(self, obj):
return obj.metadata().created_at().strftime('%Y-%m-%dT%H:%M:%S%z')
The Method and Function fields are actually non-beloved offsprings here as they do not stand for a particular type but change the way you access data. Other field types actually expect data to be accessed via properties. A bit of self promotion to not leave you with no answers: I'm writing a library similar to Marshmallow that has this part done right. Check it out at https://github.com/maximkulkin/lollipop
This is simple enough to implement as a custom field.
from marshmallow import fields
class TypedMethod(fields.Method):
def __init__(self, inner_field, *args, **kwargs):
self.inner_field = inner_field
super().__init__(*args, **kwargs)
def _deserialize(self, value, attr, data):
preprocessed = super()._deserialize(value, attr, data)
return self.inner_field.deserialize(preprocessed)
def _serialize(self, value, attr, obj):
preprocessed = super()._serialize(value, attr, obj)
return self.inner_field._serialize(preprocessed, attr, obj)
Usage:
import datetime as dt
from marshmallow import Schema, fields
class MySchema(Schema):
created_at = TypedMethod(fields.DateTime('%Y-%m-%dT%H:%M:%S%z'), serialize='_created_at')
def _created_at(self, obj):
return obj.creation_time
class Thing:
def __init__(self):
self.creation_time = dt.datetime.utcnow()
schema = MySchema()
obj = Thing()
data = schema.dump(obj).data
assert isinstance(data['created_at'], str)
assert data['created_at'] == obj.creation_time.strftime('%Y-%m-%dT%H:%M:%S%z')
Closing this for now. I'm going to hold off on adding this to core, as the custom field above is a simple workaround. We can reopen if there is further interest in adding this to core.
@sloria I think the main problem here that the field's schema and the data being returned on serialization are mixed up.
So, thinking from the architectural point of view first we need to provide a schema, such as fields.DateTime
and then we would like to get the data for the field from an unusual source, say other field or a method. It could look like this
class MySchema(Schema):
created_at = fields.DateTime('%Y-%m-%dT%H:%M:%S%z', from_field='creation_time')
or for a more complex cases
class MySchema(Schema):
created_at = fields.DateTime('%Y-%m-%dT%H:%M:%S%z', from_method='get_created_at')
def get_created_at(self, obj):
return obj.creation_time or datetime.now()
In such a case fields.Method
becomes redundant or at least rarely needed while keeping any plugin happy knowing the exact schema.
from_field
already exists. It is the attribute
parameter.
We could add parameters dump_method
and load_method
. And deprecate Method
field.
And likewise *_function
, unless there is a way to do both functions and methods with the same parameter.
@lafrech It would be great!
@lig you may open an issue for this, to see how the idea is received, and eventually work on a PR.
@lafrech I believe, this issue is exactly about this
Alright. Let's reopen this.
Yes, this would be a nicer feature to have. My current workaround is to create one attribute for serialization and one for deserialization.
Example:
# Used during Deserialization
foo = Nested(
String,
load_only=True,
)
# Used during Serialization
_foo = Method(
'serialize_foo',
data_key='foo',
dump_only=True,
)
I like this too. It would solve this: https://github.com/marshmallow-code/marshmallow/issues/1638
Any progress on this?