marshmallow icon indicating copy to clipboard operation
marshmallow copied to clipboard

fields.Nested should optionally pass the parent object to the Nested Schema

Open remeika opened this issue 7 years ago • 12 comments

As described in #900, there is no obvious way to create a nested serialization from a flat object. To fix this, fields.Nested should accept an optional from_parent argument. When true, the nested Schema is passed the parent object rather than the attribute at the field name. from_parent will default to False, which will maintain current behavior.

Any feedback on this idea? I plan on implementing it soon.

Thanks, James

remeika avatar Sep 07 '18 20:09 remeika

I'm not opposed to the idea. Feel free to send a PR for review. However, I can't promise we'll get to it in the immediate future because we're currently focused on the 3.0 final release.

sloria avatar Oct 18 '18 13:10 sloria

I don't think this should be a concern of the serialization layer really.

Maybe a fix for the problem described in #900 is to add a computed property:

@property
def meta(self):
    return {'created': self.created, 'updated': self.updated}

timc13 avatar Oct 19 '18 22:10 timc13

@timc13 That's a good point. @remeika Is your use case solved by using a computed property like in Tim's example?

sloria avatar Oct 21 '18 18:10 sloria

Related SO question: https://stackoverflow.com/questions/50485096/. I proposed @timc13's solution in an answer.

Shortcomings:

  • Boilerplate: need to create a mapping in the property function, while Marshmallow takes care of that in normal cases (including things like attribute and data_key)

  • I was gonna say it is one-way only, but I suppose you could also make an equivalent setter, it will just be even more boilerplate

lafrech avatar Oct 21 '18 19:10 lafrech

@timc13 That is a totally valid workaround and addresses my case. But I still think this feature should be added, and here is why:

  1. Minor reshaping of one's data is a valid concern of the serialization layer. Marshmallow already has several affordances to perform more complex transformations of the source object: Method/Function fields come to mind, which already allow access to the parent object within a field.
  2. This feature will allow many more cases to use a purely declarative serializer. This is quite subjective, but I think less imperative code in a Serializer implementation is always a primary goal.

remeika avatar Oct 25 '18 04:10 remeika

Could this be addressed by adding a pre_dump method that would copy the parent into the nested field?

class MySchema(Schema):

    nested = fields.Nested()
    ...

    @pre_dump
    def prepare_nested(item)
        item['nested'] = item
        return item

This way, the field does not have to access parent object. See https://github.com/marshmallow-code/marshmallow/issues/1046.

lafrech avatar Nov 16 '18 13:11 lafrech

+1 - I could really use this feature. The legacy structure I'm adapting can have nested fields at multiple levels, and it'd be really great to understand which one a field belongs to.

dsully avatar Dec 15 '18 19:12 dsully

@lafrech Isolating fields from each other is a great idea, but is an implementation detail.

remeika avatar Dec 19 '18 14:12 remeika

Hi, It feels a little weird to set a property on the object while serializing it.

To work around this, we did something like this:

def identity_accessor(attr, obj, default):
    return obj or default


class Section(fields.Nested):
    def get_value(self, attr, obj, accessor=None, default=missing):
        return super(Section, self).get_value(attr, obj,
                                              accessor=identity_accessor,
                                              default=default)

Each Section field receives the full object.

m-a-choquette avatar Dec 19 '18 15:12 m-a-choquette

Best way to solve this problem is to set the "attribute" kwarg on the Nested field itself to a callable that returns the root object:

bar_schema = api.model("Bar", {
    "id": fields.String(),
    attribute="bar_id"
})

foo_schema = api.model("Foo", {
    "bar": fields.Nested(
        bar_schema,
        attribute=lambda root_obj: root_obj)})

{"bar_id": "123"} => {"bar": {"id": "123"}}

dlavelle7 avatar Jul 17 '19 11:07 dlavelle7

I also solved this using a (simpler) variant of @m-a-choquette's method:

class SelfNested(fields.Nested):
    def get_value(self, obj, attr, accessor=None, default=missing):
        return obj

This let me use this schema:

class SampleFilterSchema(with_metaclass(SchemaMeta, BaseModelSchema)):
    class Meta:
        model = SampleFilter

    id = fields.Integer(attribute='sample_filter_id')
    type = fields.Constant('filter')
    attributes = SelfNested(Schema.from_dict(dict(
        tag=fields.String(attribute='sample_filter_tag'),
        name=fields.String(attribute='sample_filter_name'),
        public=fields.Boolean(attribute='is_public'),
        data=JsonString(attribute='sample_filter_data'),
        user=ResourceHyperlink(endpoint='rest_api.user', url_args=[
            'user_id',
        ])
    )))

(ignore my custom fields, they're not important for this example)

Which transforms:

{  
   "user_id":1,
   "is_public":false,
   "sample_filter_name":"TestFilter",
   "sample_filter_data":"",
   "sample_filter_tag":"Global",
   "sample_filter_id":1
}

into

{
   "id":1,
   "attributes":{
      "user":"/rest_api/v1/users/1",
      "tag":"Global",
      "public":false,
      "name":"TestFilter",
      "data": ""
   }
}

aka, a valid response (or segment thereof) under the JSON API standard!

multimeric avatar Aug 22 '19 08:08 multimeric

There's discussion on https://github.com/marshmallow-code/marshmallow/issues/1315#issuecomment-518090992 to allow data_key to be a nested path specified as a list of strings which would address the issue here, I think. Please comment there (or even 👍 👎 ) if you have thoughts.

sloria avatar Aug 29 '19 00:08 sloria