django-ninja icon indicating copy to clipboard operation
django-ninja copied to clipboard

Inheritance with ModelSchema?

Open ddahan opened this issue 2 years ago • 9 comments

I have two similar ModelSchema and to avoid useless repetition, I'd like to use an abstract ModelSchema to group common fields. I didn't find a mention of this in this page so I don't know if:

  1. it's possible and documented somewhere else
  2. it's possible but undocumented
  3. it's impossible

For example I have this:

class ItemInBasesSchema(ModelSchema):
    is_favorite: bool = None

    class Config:
        model = Item
        model_fields = (
            "id",
            "slug",
            "name",
            "image_path",
            "length_in_mn",
            "special_field_for_base" # specific to this schema
        )


class ItemInMealsSchema(ModelSchema):
    is_favorite: bool = None

    class Config:
        model = Item
        model_fields = (
            "id",
            "slug",
            "name",
            "image_path",
            "length_in_mn",
            "special_field_for_meal"  # specific to this schema
        )

And I would like to do something like:

class ItemBase(ModelSchema):
    is_favorite: bool = None

    class Config:
        model = Item
        model_fields = (
            "id",
            "slug",
            "name",
            "image_path",
            "length_in_mn",
        )


class ItemInBasesSchema(ItemBase):
    pass  # + get the ability to add specific fields here


class ItemInMealsSchema(ItemBase):
    pass  # + get the ability to add specific fields here

As inheritance is a very common need, I think we should mention it in the documentation (even if it's currently impossible). What do you think?

ddahan avatar Feb 04 '22 16:02 ddahan

Will this work?

class ItemBase(ModelSchema):
    is_favorite: bool = None

    class Config:
        model = Item
        model_fields = (
            "id",
            "slug",
            "name",
            "image_path",
            "length_in_mn",
        )

class ItemInBasesSchema(ItemBase, ModelSchema):
    class Config(ItemBase.Config):
        model_fields = ItemBase.Config.model_fields + ("specific_field", )

class ItemInMealsSchema(ItemBase, ModelSchema):
    class Config(ItemBase.Config):
        model_fields = ItemBase.Config.model_fields + ("specific_field1", )

mom1 avatar Feb 04 '22 16:02 mom1

@mom1 thanks a lot, this is the way! (it seems it even work without inheriting from ItemBase.Config for Config class) @vitalik do you want me to make a PR to add such an example in the documentation?

ddahan avatar Feb 04 '22 17:02 ddahan

sure all PR are welcome especially on docs

vitalik avatar Feb 04 '22 20:02 vitalik

@mom1 It seems I made a mistake. Even if I have no error using your solution, specific_field is not added to ItemInBasesSchema. ItemInBasesSchema has the same fields that ItemBase.

ddahan avatar Feb 05 '22 13:02 ddahan

@ddahan yep, try adding ModelSchema as a second parent.

mom1 avatar Feb 05 '22 14:02 mom1

Thanks! Here is an example of the related doc, what do you think?


Inheritance

You can extend and reuse schemas through inheritance. This allows you to declare a common set of fields or methods on a parent class that can then be used in a number of schemas. For example,

class SimpleUserSchema(ModelSchema):
    class Config:
        model = User
        model_fields = ["id", "username", "email"]


class FullUserSchema(SimpleUserSchema):
    class Config(SimpleUserSchema.Config):
        model_fields = SimpleUserSchema.Config.model_fields + ["first_name", "last_name"]

Note from the examble above, that the inner Config class on schemas does not implicitly inherit from it's parents inner Config classes. If you want the Config class to inherit from a parent class you must do so explicitly.

ddahan avatar Feb 06 '22 13:02 ddahan

Same question, but with arbitrary models. I want to be able to exclude the id field by default on my input schemas fo all models, but can't define a base ModelSchema it seems without also specifying the model. Is there a workaround?

i.e. this doesn't work because BaseInModel doesn't define a model:

class BaseInModel(ModelSchema):
    class Meta:
        exclude = ['id']

class BrandSchemaIn(BaseInModel):
    class Meta:
        model = Brand
        # exclude = ['id']
        fields_optional = '__all__'

boosh avatar Dec 15 '23 09:12 boosh

Inner classes do not inherit when redeclared, so it wouldn't help you to do that even if it was allowed.

What you should be able to do is just create a base metaclass, like this:

class BaseInMeta:
    exclude = ['id']

class BrandSchemaIn(ModelSchema):
    class Meta(BaseInMeta):
        model = Brand
        fields_optional = '__all__'

OtherBarry avatar Dec 15 '23 13:12 OtherBarry

Oh thanks, I didn't know you could do that

boosh avatar Dec 15 '23 14:12 boosh