marshmallow icon indicating copy to clipboard operation
marshmallow copied to clipboard

Can/should marshmallow include pre-built validates_schema methods?

Open sirosen opened this issue 2 years ago • 3 comments

I have a case in which I'm looking at adding a mutually exclusive set of fields to a schema. It would be awesome if I could define this behavior with marshmallow.validate.MutuallyExclusive(["foo", "bar"]), and I'd be happy to work on an implementation of that. But where would it go and how would users apply it?

Unless I've missed/forgotten something, there's no way to provide pre-packaged schema validators for users to apply.

The following could work but is ugly:

class MySchema(Schema):
    foo = fields.String()
    bar = fields.String()

    _foo_bar_mutex = validates_schema(MutuallyExclusive(["foo", "bar"]))

What about a class decorator which adds a validator?

@with_validates_schema(MutuallyExclusive(["foo", "bar"]))
class MySchema(Schema):
    foo = fields.String()
    bar = fields.String()

Is this worth pursuing? And if so, what is the correct interface to build?

sirosen avatar May 23 '22 15:05 sirosen

I would consider mutually exclusive fields to be discriminators for polymorphism. As far as I can tell none of the community polymorphism libraries are quite as concise as your examples, so they are probably not going to save you any lines over just rolling schema validators as needed.

Another syntax you might consider is a custom base schema with a field registry.

class MySchema(MutexSchema):
    foo = Mutex(fields.String())
    bar = Mutex(fields.String())

A more complicated use case you might consider is supporting mutex groups:

alpha = Mutex()
beta = Mutex()

# (foo XOR (bar AND baz)) AND (fizz XOR buzz)
class MySchema(MutexSchema):
    foo = alpha(fields.String())
    bar = alpha(fields.String())
    baz = alpha(fields.String(), group='bar')
    fizz = beta(fields.String())
    buzz = beta(fields.String())

I generally try to build extensions as modules in my project, work out any kinks after using it a few times, then publish it as a community module once it has stabilized. That actually reminds me, I have a community module for a schema meta decorator I need to publish soon. 😄

deckar01 avatar May 23 '22 21:05 deckar01

Yep, I agree that it's a kind of polymorphism! It can get tricky to model with mutex, since the number of variants is equal to the product of sizes of mutex groups, which can get a bit big.

Another syntax you might consider is a custom base schema with a field registry.

This looks very cool. I'll look into that for my own purposes for sure; thanks for sharing the idea! It could also possibly be pulled from field metadata, e.g. fields.String(metadata={"mutex_group": alpha}).

I don't want to focus overly much of mutex groups -- that's a motivating example, but not my only area of interest.

I realized that it isn't very smooth, with the current interfaces, to share a schema-level validator as something reusable, even within my own project. To me, that looks like a gap in the API, but it might not be worth filling.

I just thought of this way of adding it to the core:

class MySchema(Schema, validate=MutuallyExclusive(["foo", "bar"])):
    foo = fields.String()[
    bar = fields.String()

That has really nice symmetry with the validate arg to Field classes, but it requires work in SchemaMeta so I'm a bit wary of it.

sirosen avatar May 24 '22 15:05 sirosen

That's not a pattern I have seen in other libraries, so I would probably avoid it. The schema decorator syntax would work, but you still have to make the referenced fields optional. Communicating that behavior explicitly by wrapping field attributes has the benefit of avoiding rebuilding the schema with instrumented fields.

You could make a decorator for injecting schema validation methods using the same pattern I used for my meta decorator.

https://github.com/deckar01/marshmallow-meta/blob/master/src/marshmallow_meta/init.py

def schema_validator(**validators):
    def wrapper(schema):
        return type(schema.__name__, (schema,), validators)
    return wrapper

@schema_validator(mutex=MutuallyExclusive(["foo", "bar"])
class MySchema(Schema):
    ...

deckar01 avatar May 25 '22 06:05 deckar01