Допустим, у меня есть модель Pydantic с проверкой:
Name = Annotated[str, AfterValidator(validate_name)]
class Foo(BaseModel):
id: UUID = Field(default_factory=uuid4)
name: Name
И конечная точка FastAPI:
@app.post('/foos')
def create_foo(foo: Foo) -> Foo:
save_to_database(foo)
return foo
Я хочу, чтобы вызывающая сторона могла передавать значение только для name, но не для id. Есть ли способ сделать что-то подобное?
def create_foo(foo: Annotated[Foo, Body(include=['id'])]) -> Foo:
Я знаю, что могу:
@app.post('/foos')
def create_foo(name: Annotated[str, Body(embed=True)]) -> Foo:
foo = Foo(name=name)
save_to_database(foo)
return foo
Но тогда неявная обработка ошибок проверки больше не работает, и для этого мне нужно добавить больше кода.
Есть ли элегантный способ справиться с этим?
Да, модель отдельного запроса тоже возможна, но имеет другие недостатки. Поэтому я хотел узнать, я просто что-то упускаю из виду или для этого действительно нет ничего встроенного.






Вы можете скрыть/исключить поле при создании/создании объекта, используя Атрибуты частной модели. Pydantic исключит атрибуты модели, имеющие начальное подчеркивание. Как описано в связанной документации:
Атрибуты, имя которых имеет начальное подчеркивание, не рассматриваются как атрибуты. поля от Pydantic и не включены в схему модели. Вместо, они преобразуются в «частный атрибут», который не проверяется. или даже устанавливаться во время звонков на
__init__,model_validateи т. д.
Однако обратите внимание, что:
Начиная с Pydantic v2.1.0, вы получите
NameError, если попытаетесь использовать функция Поле с частным атрибутом. Потому что частный атрибуты не рассматриваются как поля (как упоминалось ранее), функциюField()применить нельзя.
Таким образом, в Pydantic V2 вы можете использовать функцию PrivateAttr вместо функции Field вместе с параметром default_factory, чтобы определить вызываемый объект, который будет вызываться для генерации динамического значения по умолчанию (т. е. разного для каждого экземпляр модели) — в данном случае UUID.
from fastapi import FastAPI
from pydantic import BaseModel, PrivateAttr
from uuid import UUID, uuid4
class Foo(BaseModel):
_id: UUID = PrivateAttr(default_factory=uuid4)
name: str
app = FastAPI()
@app.post("/foo")
def create_foo(foo: Foo):
print(foo._id)
return foo
Просто вариант вышеизложенного (подробнее см. в документации), без использования PrivateAttr и default_factory. Вместо этого метод __init__ используется напрямую для автоматической генерации нового UUID.
from fastapi import FastAPI
from pydantic import BaseModel
from uuid import UUID, uuid4
class Foo(BaseModel):
_id: UUID
name: str
def __init__(self, **data):
super().__init__(**data)
self._id = uuid4()
app = FastAPI()
@app.post("/foo")
def create_foo(foo: Foo):
print(foo._id)
return foo
Другой способ — использовать две разные модели Pydantic: одну, предназначенную для использования пользователем, а вторую, которая должна наследовать от первой (базовой) модели, серверной частью. Подобные примеры приведены в документации FastAPI Дополнительные модели , а также в Полнофункциональный шаблон FastAPI.
from fastapi import FastAPI
from pydantic import BaseModel, Field
from uuid import UUID, uuid4
class BaseFoo(BaseModel):
name: str
class Foo(BaseFoo):
id: UUID = Field(default_factory=uuid4)
app = FastAPI()
@app.post("/foo")
def create_foo(base: BaseFoo):
foo = Foo(**base.model_dump()) # Foo(name=base.name) should work as well
print(foo.id)
return base
Это вариант вариантов 1 и 2, модифицированный таким образом, чтобы можно было определить их частный атрибут без использования подчеркивания (если это требуется в вашем проекте), но при этом получить тот же результат, что и при использовании любого из предыдущих вариантов. были использованы.
В этом случае используется функция Field. Поскольку атрибут должен быть скрыт от клиента, вам необходимо установить для атрибута exclude значение True, чтобы, если вы вернете экземпляр модели Pydantic обратно клиенту, этот атрибут не будет включен. Кроме того, в Pydantic V2 вы можете использовать аннотацию SkipJsonSchema , чтобы пропустить это поле из сгенерированной схемы JSON, как, например, в автодокументах Swagger UI (для решений Pydantic V1 ознакомьтесь с этим Пост на github и связанное с ним обсуждение).
Теперь нет ничего, кроме как помешать клиенту передать скрытый атрибут в теле запроса JSON, независимо от его сокрытия из схемы и определения его как необязательного/необязательного (т. е. Field(default=None)). Поскольку в этом решении используется обычный Field, а не PrivateAttr, используется атрибут default_factory, как в варианте 1 и как показано ниже:
class Foo(BaseModel):
id: SkipJsonSchema[UUID] = Field(default_factory=uuid4, exclude=True)
name: str
будет не самым подходящим подходом, поскольку если бы клиент передал значение для id, это значение было бы присвоено этому полю.
Однако, используя подход, аналогичный варианту 2, т. е. заменяя default_factory на __init__ (который используется для создания поля UUID для id), даже если клиент передал значение для id, оно будет «игнорировано», а сгенерированное по модели будут назначены полю.
from fastapi import FastAPI
from pydantic import BaseModel, Field
from uuid import UUID, uuid4
from pydantic.json_schema import SkipJsonSchema
class Foo(BaseModel):
id: SkipJsonSchema[UUID] = Field(default=None, exclude=True)
name: str
def __init__(self, **data):
super().__init__(**data)
self.id = uuid4()
app = FastAPI()
@app.post("/foo")
def create_foo(foo: Foo):
print(foo.id)
return foo
Этот ответ и этот ответ также могут оказаться полезными для будущих читателей.
default_factory или __init__ перезаписывания предоставленного значения поляХотя это не является проблемой при использовании варианта 3, представленного выше (и при желании можно выбрать этот вариант), это может возникнуть при использовании одного из оставшихся вариантов, в зависимости от метода, используемого для заполнения модели.
Например, если вы заполняете модель, используя Foo(**data), где data — это уже существующий экземпляр модели из вашей базы данных, используя один из вариантов, описанных ранее (кроме варианта 3, который не страдает от этой проблемы), значение _id или id (включенное в в словаре data), переданный в модель, будет заменен/перезаписан вновь сгенерированным.
Для решения этой проблемы предлагаются следующие решения.
После вызова Foo(**data) вы можете просто заменить вновь сгенерированный идентификатор существующим, установив значение для этого конкретного поля на data["_id"] или data["id"] (в зависимости от используемой опции).
Пример:
data = {"name": "foo", "_id": "7c4308a2-0f32-1243-b2ad-bf214a24a5aa"}
f = Foo(**data)
f._id = data["_id"]
Вместо использования Foo(**data), который использует метод __init__ модели и, следовательно, при вызове генерируется новое значение id — для полноты картины следует также отметить, что существует дополнительный метод, т. е. model_validate(), который очень похож на метод __init__ модели, за исключением того, что он принимает словарь или объект, а не аргументы ключевого слова — можно было использовать метод model_construct() (в Pydantic V1 раньше это было construct()).
Согласно документации :
Создание моделей без проверки
Pydantic также предоставляет метод model_construct(), который позволяет создавать модели без проверки. Это может быть полезно по крайней мере в нескольких случаях:
- при работе со сложными данными, о которых уже известно, что они действительны (из соображений производительности)
- когда одна или несколько функций валидатора неидемпотентны, или
- когда одна или несколько функций валидатора имеют побочные эффекты, которые вы не хотите запускать.
Предупреждение
model_construct()не выполняет никакой проверки, то есть может создавать недействительные модели. Вам следует использовать толькоmodel_construct()метод с уже проверенными данными, или которому вы определенно доверяете.[...]
При создании экземпляра с использованием
model_construct()нет__init__будет метод из модели или любого из ее родительских классов вызывается, даже если определен собственный метод__init__.
Пример:
data = {"name": "foo", "_id": "7c4308a2-0f32-1243-b2ad-bf214a24a5aa"}
f = Foo.model_construct(**data)
Это не те решения, на которые я надеялся, но я признаю, что лучшего пути нет. Спасибо!
Пожалуйста. Могу ли я спросить, например, в чем проблема с вариантом 1 или 2? Неужели вы не можете напечатать/передать в базу данных частный атрибут, т. е. _id, при использовании объекта foo и его нужно вызывать индивидуально, например, foo._id?
Просто кажется, что всегда приходится использовать _id вместо обычного id, что оказывает ненужное влияние на остальную часть кода. В моем случае я также сериализую это в простое хранилище NoSQL, где _ может показаться странным, поэтому мне придется выполнить там дополнительное сопоставление.
Я только что добавил четвертый вариант, который может удовлетворить ваши требования (однако я все равно, вероятно, выберу один из предыдущих вариантов).
Ммм, это как бы противоположность желаемому. id должен быть включен в ответ JSON и не должен генерироваться с нуля каждый раз, когда используется модель. Я просто не хочу, чтобы клиент мог сам отправлять или помещать идентификатор. Я думаю, что это довольно распространенный вариант использования на самом деле‽
«В ответ JSON следует включить id...» — в этом случае вы можете просто удалить exclude=True. «... не следует генерировать с нуля каждый раз, когда используется модель». - Вся цель default_factory (или альтернативного способа, упомянутого в документации и продемонстрированного в вариантах 2 и 4), заключается в том, что вам нужно значение поля ️ 🔁 «быть динамичным (т. е. разным для каждой модели)» , как в случае с UUID.
Да, я хочу, чтобы для каждого нового экземпляра оно было разным. Я не хочу, чтобы идентификатор перезаписывался вновь сгенерированным идентификатором каждый раз, когда я восстанавливаю уже существующий экземпляр из своей базы данных с помощью Foo(**data).
Ответ выше был обновлен соответствующими решениями. Надеюсь, что это работает для вас.
Думаю, это вариант, спасибо. На данный момент я использовал свой последний образец, который я показал в своем вопросе + try..except для обработки ошибок проверки; но для более сложных случаев с большим количеством параметров я рассмотрю одно из ваших предложений.
Я думаю, что общий способ сделать это — иметь
BaseFooModel, у которого есть только имя, а затем наследовать его, чтобы создать модель, имеющуюid. некоторые похожие примеры github.com/tiangolo/full-stack-fastapi-template/blob/master/…