I want a pydantic Model that can take exactly one of two optional arguments, where the missing argument will be calculated from the other.
Consider this:
from pydantic import BaseModel, model_validator
from typing import Optional
class Object:
id: int
def get_object_by_id(id: int) -> Object:
...
class ObjectContent(BaseModel):
_object_id: Optional[int] = None
_object: Optional[Object] = None
@model_validator(mode="before")
@classmethod
def object_reference(cls, values):
if values.get("_object_id") is None and values.get("_object") is None:
raise ValueError("Define either _object_id or _object")
@property
def object_id(self) -> int:
if self._object_id is None:
self._object_id = self.object.id
return self._object_id # type check complains that this might still be None
@object_id.setter
def object_id(self, id: int):
self._object_id = id
@property
def object(self) -> Object:
if self._object is None:
self._object = get_object_by_id(self.object_id)
return self._object # type check complains that this might still be None
@object.setter
def object(self, object: Object):
self._object = object
ObjectContent(_object_id=5) # I would prefer to initialize with the unprotected argument, i.e. ObjectContent(object_id=5) or ObjectContent(object=my_object)
get_object_from_id would be a costly io-operation it should stay lazy loadedUse real fields object_id and alias input object; keep a private cache so fetch stays lazy.
Enforce XOR in a model validator.
Fill object_id if an object was provided so types are non-optional afterwards.
from __future__ import annotations
from pydantic import BaseModel, Field, PrivateAttr, model_validator
from typing import Optional
class Object:
id: int
def get_object_by_id(id: int) -> Object:
...
class ObjectContent(BaseModel):
object_id: Optional[int] = None
_object_in: Optional[Object] = Field(default=None, alias="object")
_obj_cache: Optional[Object] = PrivateAttr(None)
@model_validator(mode="after")
def _xor_and_fill(self):
if (self.object_id is None) == (self._object_in is None):
raise ValueError("provide exactly one of object_id or object")
if self._object_in is not None:
self.object_id = self._object_in.id
return self
@property
def object(self) -> Object:
if self._obj_cache is None:
if self._object_in is not None:
self._obj_cache = self._object_in
else:
assert self.object_id is not None
self._obj_cache = get_object_by_id(self.object_id) # lazy IO
return self._obj_cache
Usage:
ObjectContent(object_id=5) - model.object triggers lazy fetch
ObjectContent(object=my_obj) - model.object_id == my_obj.id and no fetch needed