marshmallow-code/marshmallow

Feature request: dynamic field type

matejsp opened this issue · 1 comments

I would like to have dynamic field that resolves its type based on data. One way is to create schema from dict.

Example (data based on section value):

        'section': 'basic',
        'data': {
            'occupation': 'bla',
            'profession': 'bla',
        }

or

        'section': 'extended',
        'data': {
            'place_of_birth': 'bla',
            'country': 'bla',
        }

I have prepared one simple version just to get a feedback if you like it and can be part of marshmallow.
Essentially DynamicField accepts a dynamic lambda that returns the field based on data and/or context. And then it delegates _deserialize and _serialize to that field.

import typing

import marshmallow
from marshmallow.fields import Field, String


class DynamicField(marshmallow.fields.Field):

    def __init__(
         self,
         dynamic: typing.Callable[[typing.Any, typing.Any, typing.Any], Field],
         **kwargs,
     ):
        super().__init__(
            **kwargs
        )
        self.dynamic = dynamic

    def get_field(self, data):
        context = getattr(self.parent, "context", {})
        field = self.dynamic(self.parent, data, context)
        return field

    def _serialize(self, value, attr, obj, **kwargs) -> typing.Any | None:
        if value is None:
            return None

        field = self.get_field(obj)
        return field._serialize(value, attr, obj, **kwargs)

    def _deserialize(self, value, attr, data, **kwargs) -> typing.Any:
        field = self.get_field(data)
        return field._deserialize(value, attr, data, **kwargs)


# Example
class BasicSectionSchema(marshmallow.Schema):
    occupation = String(required=True)
    profession = String(required=True)


class ExtendedSectionSchema(marshmallow.Schema):
    place_of_birth = String(required=True)
    country = String(required=True)


class SubmitSectionSchema(marshmallow.Schema):

    def resolve_field(self, data, context):
        if data.get("section", None) == "basic":
            return marshmallow.fields.Nested(BasicSectionSchema)
        elif data.get("section", None) == "extended":
            return marshmallow.fields.Nested(ExtendedSectionSchema)
        else:
            return marshmallow.fields.String()

    section = String(required=True)
    data = DynamicField(dynamic=resolve_field, required=True)


if __name__ == '__main__':
    schema = SubmitSectionSchema()
    schema.load({
        'section': 'basic',
        'data': {
            'occupation': 'bla',
            'profession': 'bla',
        },
    })

    schema.load({
        'section': 'extended',
        'data': {
            'place_of_birth': 'bla',
            'country': 'bla',
        },
    })

    schema.load({
        'section': 'junk',
        'data': 'some junk',
    })

    try:
        schema.load({
            'section': 'basic',
            'data': {
                'profession': 'bla',
            },
        })
    except marshmallow.ValidationError as e:
        print(e.messages)

    try:
        schema.load({
            'section': 'extended',
            'data': {
                # 'profession': 'bla',
            },
        })
    except marshmallow.ValidationError as e:
        print(e.messages)

    try:
        schema.load({
            'section': 'junk',
            'data': {
                # 'profession': 'bla',
            },
        })
    except marshmallow.ValidationError as e:
        print(e.messages)

Search for marshmallow-oneofschema (and perhaps marshmallow-polyfield). And polymorphism tag in here.