marshmallow icon indicating copy to clipboard operation
marshmallow copied to clipboard

fields.Method result should be able to be wrapped into other field types

Open gukoff opened this issue 7 years ago • 11 comments

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')

gukoff avatar Aug 18 '16 10:08 gukoff

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

maximkulkin avatar Aug 18 '16 18:08 maximkulkin

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 avatar Mar 19 '17 23:03 sloria

@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.

lig avatar Oct 12 '18 11:10 lig

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 avatar Oct 12 '18 12:10 lafrech

@lafrech It would be great!

lig avatar Oct 12 '18 13:10 lig

@lig you may open an issue for this, to see how the idea is received, and eventually work on a PR.

lafrech avatar Oct 12 '18 13:10 lafrech

@lafrech I believe, this issue is exactly about this

lig avatar Oct 12 '18 13:10 lig

Alright. Let's reopen this.

lafrech avatar Oct 12 '18 13:10 lafrech

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,
)

richard-to avatar Dec 26 '18 22:12 richard-to

I like this too. It would solve this: https://github.com/marshmallow-code/marshmallow/issues/1638

kettenbach-it avatar Jul 21 '20 15:07 kettenbach-it

Any progress on this?

1Mark avatar Aug 09 '22 20:08 1Mark