Reputation: 1664
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
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
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