0

I am trying to build a marshmallow scheme to both load and dump data. And I get everything OK except one field.

Problem description

(If you understand the problem, you don't have to read this).

For load data its type is Decimal. And I used it like this before. Now I want to use this schema for dumping and for that my flask API responses with: TypeError: Object of type Decimal is not JSON serializable. OK, I understand. I changed the type to Float. Then my legacy code started to get an exception while trying to save that field to database (it takes Decimal only). I don't want to change the legacy code so I looked for any solution at the marshmallow docs and found load_only and dump_only params. It seems like those are what I wanted, but here is my problem - I want to set them to the same field. So I just wondered if I can define both fields and tried this:

class PaymentSchema(Schema):
    money = fields.Decimal(load_only=True)
    money = fields.Float(dump_only=True)

I have been expected for a miracle, of course. Actually I was thinking that it will skip first definition (correctly, re-define it). What I got is an absence of the field at all.

Workaround solution

So I tried another solution. I created another schema for dump and inherit it from the former schema:

class PaymentSchema(Schema):
    money = fields.Decimal(load_only=True)

class PaymentDumpSchema(PaymentSchema):
    money = fields.Float(dump_only=True)

It works. But I wonder if there's some another, native, "marshmallow-way" solution for this. I have been looking through the docs but I can't find anything.

egvo
  • 1,493
  • 18
  • 26

3 Answers3

2

pass data_key argument to the field definition

Documentation mentions, data_key parameter can be used along with dump_only or load_only to be able to have same field with different functionality. attribute parameter's value will refer the field with its original field name.

So you can write your schema as...

class PaymentSchema(Schema):
    decimal_money = fields.Decimal(data_key="money", load_only=True, attribute="money")
    money = fields.Float(dump_only=True)

This should solve your problem. I am using data_key for similar problem in marshmallow with SQLAlchemyAutoSchema and this fixed my issue.

Note: The key in ValidationError.messages (error messages) will be decimal_money by default. You may tweak the handle_error method of Schema class to replace decimal_money with money but it is not recommended as you yourself may not be able to differentiate between the error messages fields.

Thanks.

  • 1
    Seems like a best solution. Thank you! Can't check it right now, alas. – egvo Dec 19 '22 at 13:31
  • 1
    Okay, test it and if works out then kindly accept answer & upvote so that it could come on the top :) – Karishma Sukhwani Dec 29 '22 at 08:26
  • change second line to `decimal_money = fields.Decimal(data_key="money", load_only=True, attribute='money')`, remove `Edit` part and I will accept your answer. I tested it. `data_key` does half of the trick (it gets value from `money` field), the other part is to rename `decimal_money` to `money` which `attribute` does. Thank you for your solution! – egvo Apr 05 '23 at 13:05
  • @egvo Yes, thanks for reminding about attribute. I used attribute field as I wanted to relate `SQLAlchemyAutoSchema` field with Model, but did not mention here before. I have added necessary changes in answer now as suggested. Glad that it helped, You may accept now :) – Karishma Sukhwani Apr 05 '23 at 21:33
  • are you sure about `Note` part considering last changes? – egvo Apr 08 '23 at 07:14
  • Yes I was working on it and came across error dictionary part, so I added that note in my answer – Karishma Sukhwani Apr 11 '23 at 21:21
1

You can use the marshmallow decorator @pre_load in this decorator you can do whatever you want and return with your type

from marshmallow import pre_load

import like this and in this you will get your payload and change the type as per your requirement.

Prashant Suthar
  • 292
  • 3
  • 11
1

UPD: I found a good solution finally.

NEW SOLUTION

The trick is to define your field in load_fields and dump_fields inside __init__ method.

from marshmallow.fields import Integer, String, Raw
from marshmallow import Schema


class ItemDumpLoadSchema(Schema):
    item = Raw()

    def __init__(self, *args, **kwargs):
        super().__init__(*args, **kwargs)
        if not (self.only and 'item' not in self.only) and \
                not (self.exclude and 'item' in self.exclude):
            self.load_fields['item'] = Integer(missing=0)
            self.dump_fields['item'] = String()

Usage:

>>> ItemDumpLoadSchema().load({})
{'item': 0}
>>> ItemDumpLoadSchema().dump({'item': 0})
{'item': '0'}

Don't forget to define field in a schema with some field (Raw in my example) - otherwise it may raise an exception in some cases (e.g. using of only and exclude keywords).

OLD SOLUTION

A little perverted one. It based on @prashant-suthar answer. I named load field with suffix _load and implemented @pre_load, @post_load and error handling.

class ArticleSchema(Schema):
    id = fields.String()
    title = fields.String()
    text = fields.String()
    
    
class FlowSchema(Schema):
    article = fields.Nested(ArticleSchema, dump_only=True)
    article_load = fields.Int(load_only=True)

    @pre_load
    def pre_load(self, data, *args, **kwargs):
        if data.get('article'):
            data['article_load'] = data.pop('article')
        return data

    @post_load
    def post_load(self, data, *args, **kwargs):
        if data.get('article_load'):
            data['article'] = data.pop('article_load')
        return data

    def handle_error(self, exc, data, **kwargs):
        if 'article_load' in exc.messages:
            exc.messages['article'] = exc.messages.pop('article_load')
        raise exc

Why the old solution is not a good solution?

It doesn't allow to inheritate schemas with different handle_error methods defined. And you have to name pre_load and post_load methods with different names.

egvo
  • 1,493
  • 18
  • 26