Daniel
Daniel

Reputation: 1664

Conditionally required value in Pydantic v2 model

I'm working with an API that accepts a query parameter, which selects the values the API will return. Therefore, when parsing the API response, all attributes of the Pydantic model used for validation must be optional:

class InvoiceItem(BaseModel):
    """
    Pydantic model representing an Invoice
    """

    id: PositiveInt | None = None
    org: AnyHttpUrl | None = None
    relatedInvoice: AnyHttpUrl | None = None
    quantity: PositiveInt | None = None

However, when creating an object using the API, some of the attributes are required. How can I make attributes to be required in certain conditions (in Pydantic v1 it was possible to use metaclasses for this)?

Examples could be to somehow parameterise the model (as it wouldn't know without external input how its being used) or to create another model InvoiceItemCreate inheriting from InvoiceItem and make the attributes required without re-defining them.

Upvotes: 3

Views: 1815

Answers (2)

Daniel
Daniel

Reputation: 1664

Inspired by Marks answer I ended up using something like this:

Mixin generator pattern:

from typing import Self
from pydantic import BaseModel, model_validator


def required_mixin(required_attributes: list[str | list[str]]):


    class SomeRequired(BaseModel):

        @model_validator(mode="after")
        def required_fields(self) -> Self:
            for v in required_attributes:
                if isinstance(v, str):
                    if getattr(self, v) is None:
                        raise ValueError(f"{v} attribute is required but is None")
                else:
                    if not any(getattr(self, attr) is not None for attr in v):
                        raise ValueError(f"One of {v} is required.")

            return self

    return SomeRequired

Actual class

class InvoiceItem(BaseModel):
    """
    Pydantic model representing an Invoice
    """

    id: PositiveInt | None = None
    quantity: PositiveInt | None = None
    unitPrice: float | None = None
    totalPrice: float | None = None
    title: str | None = None
    description: str | None = None


class InvoiceItemCreate(
    InvoiceItem,
    required_mixin(["title", "quantity", ["unitPrice", "totalPrice"]]),
):
    """
    Pydantic model representing an Invoice
    """

This allows me to re-use the definitions of the actual class and create an additional model based on the old one. The model validator allows to define certain attributes that are required when using the InvoiceItemCreate model, and even allows little more complex scenarios like "either unitPrice or totalPrice is required.

Upvotes: 0

Mark
Mark

Reputation: 1089

In this solution, there I've asserted that at least one attribute is required, but you could also impose other conditions on the attributes through the model validator:

from typing import Self
from pydantic import HttpUrl, BaseModel, PositiveInt, model_validator


class AllRequired(BaseModel):
    @model_validator(mode='after')
    def not_all_none(self) -> Self:
        if all(v is None for _, v in self):
            raise ValueError('All values were none')
        return self

class InvoiceItem(AllRequired):
    """
    Pydantic model representing an Invoice
    """

    id: PositiveInt | None = None
    org: HttpUrl | None = None
    relatedInvoice: HttpUrl | None = None
    quantity: PositiveInt | None = None

    
InvoiceItem(id=None, org=None, relatedInvoice=None, quantity=None) # Invalid item

Upvotes: 1

Related Questions