Вы можете объявить тип ответа, указав аннотацию возвращаемого значения для функции операции пути.
FastAPI позволяет использовать аннотации типов таким же способом, как и для ввода данных в параметры функции, вы можете использовать модели Pydantic, списки, словари, скалярные типы (такие, как int, bool и т.д.).
FastAPI будет использовать этот возвращаемый тип для:
Валидации ответа.
Если данные невалидны (например, отсутствует одно из полей), это означает, что код вашего приложения работает некорректно и функция возвращает не то, что вы ожидаете. В таком случае приложение вернет server error вместо того, чтобы отправить неправильные данные. Таким образом, вы и ваши пользователи можете быть уверены, что получите корректные данные в том виде, в котором они ожидаются.
Добавьте JSON схему для ответа внутри операции пути OpenAPI.
Она будет использована для автоматически генерируемой документации.
А также - для автоматической кодогенерации пользователями.
Но самое важное:
Ответ будет ограничен и отфильтрован - т.е. в нем останутся только те данные, которые определены в возвращаемом типе.
Это особенно важно для безопасности, далее мы рассмотрим эту тему подробнее.
Бывают случаи, когда вам необходимо (или просто хочется) возвращать данные, которые не полностью соответствуют объявленному типу.
Допустим, вы хотите, чтобы ваша функция возвращала словарь (dict) или объект из базы данных, но при этом объявляете выходной тип как модель Pydantic. Тогда именно указанная модель будет использована для автоматической документации, валидации и т.п. для объекта, который вы вернули (например, словаря или объекта из базы данных).
Но если указать аннотацию возвращаемого типа, статическая проверка типов будет выдавать ошибку (абсолютно корректную в данном случае). Она будет говорить о том, что ваша функция должна возвращать данные одного типа (например, dict), а в аннотации вы объявили другой тип (например, модель Pydantic).
В таком случае можно использовать параметр response_model внутри декоратора операции пути вместо аннотации возвращаемого значения функции.
Параметр response_model может быть указан для любой операции пути:
Помните, что параметр response_model является параметром именно декоратора http-методов (get, post, и т.п.). Не следует его указывать для функций операций пути, как вы бы поступили с другими параметрами или с телом запроса.
response_model принимает те же типы, которые можно указать для какого-либо поля в модели Pydantic. Таким образом, это может быть как одиночная модель Pydantic, так и список (list) моделей Pydantic. Например, List[Item].
FastAPI будет использовать значение response_model для того, чтобы автоматически генерировать документацию, производить валидацию и т.п. А также для конвертации и фильтрации выходных данных в объявленный тип.
Подсказка
Если вы используете анализаторы типов со строгой проверкой (например, mypy), можно указать Any в качестве типа возвращаемого значения функции.
Таким образом вы информируете ваш редактор кода, что намеренно возвращаете данные неопределенного типа. Но возможности FastAPI, такие как автоматическая генерация документации, валидация, фильтрация и т.д. все так же будут работать, просто используя параметр response_model.
Если одновременно указать аннотацию типа для ответа функции и параметр response_model - последний будет иметь больший приоритет и FastAPI будет использовать именно его.
Таким образом вы можете объявить корректные аннотации типов к вашим функциям, даже если они возвращают тип, отличающийся от указанного в response_model. Они будут считаны во время статической проверки типов вашими помощниками, например, mypy. При этом вы все так же используете возможности FastAPI для автоматической документации, валидации и т.д. благодаря response_model.
Вы можете указать значение response_model=None, чтобы отключить создание модели ответа для данной операции пути. Это может понадобиться, если вы добавляете аннотации типов для данных, не являющихся валидными полями Pydantic. Мы увидим пример кода для такого случая в одном из разделов ниже.
Здесь мы объявили модель UserIn, которая хранит пользовательский пароль в открытом виде:
fromfastapiimportFastAPIfrompydanticimportBaseModel,EmailStrapp=FastAPI()classUserIn(BaseModel):username:strpassword:stremail:EmailStrfull_name:str|None=None# Don't do this in production!@app.post("/user/")asyncdefcreate_user(user:UserIn)->UserIn:returnuser
fromtypingimportUnionfromfastapiimportFastAPIfrompydanticimportBaseModel,EmailStrapp=FastAPI()classUserIn(BaseModel):username:strpassword:stremail:EmailStrfull_name:Union[str,None]=None# Don't do this in production!@app.post("/user/")asyncdefcreate_user(user:UserIn)->UserIn:returnuser
Информация
Чтобы использовать EmailStr, прежде необходимо установить email_validator.
Используйте pip install email-validator
или pip install pydantic[email].
Далее мы используем нашу модель в аннотациях типа как для аргумента функции, так и для выходного значения:
fromfastapiimportFastAPIfrompydanticimportBaseModel,EmailStrapp=FastAPI()classUserIn(BaseModel):username:strpassword:stremail:EmailStrfull_name:str|None=None# Don't do this in production!@app.post("/user/")asyncdefcreate_user(user:UserIn)->UserIn:returnuser
fromtypingimportUnionfromfastapiimportFastAPIfrompydanticimportBaseModel,EmailStrapp=FastAPI()classUserIn(BaseModel):username:strpassword:stremail:EmailStrfull_name:Union[str,None]=None# Don't do this in production!@app.post("/user/")asyncdefcreate_user(user:UserIn)->UserIn:returnuser
Теперь всякий раз, когда клиент создает пользователя с паролем, API будет возвращать его пароль в ответе.
В данном случае это не такая уж большая проблема, поскольку ответ получит тот же самый пользователь, который и создал пароль.
Но что если мы захотим использовать эту модель для какой-либо другой операции пути? Мы можем, сами того не желая, отправить пароль любому другому пользователю.
Осторожно
Никогда не храните пароли пользователей в открытом виде, а также никогда не возвращайте их в ответе, как в примере выше. В противном случае - убедитесь, что вы хорошо продумали и учли все возможные риски такого подхода и вам известно, что вы делаете.
В нашем примере модели входных данных и выходных данных различаются. И если мы укажем аннотацию типа выходного значения функции как UserOut - проверка типов выдаст ошибку из-за того, что мы возвращаем некорректный тип. Поскольку это 2 разных класса.
Поэтому в нашем примере мы можем объявить тип ответа только в параметре response_model.
...но продолжайте читать дальше, чтобы узнать как можно это обойти.
Продолжим рассматривать предыдущий пример. Мы хотели аннотировать входные данные одним типом, а выходное значение - другим типом.
Мы хотим, чтобы FastAPI продолжал фильтровать данные, используя response_model.
В прошлом примере, т.к. входной и выходной типы являлись разными классами, мы были вынуждены использовать параметр response_model. И как следствие, мы лишались помощи статических анализаторов для проверки ответа функции.
Но в подавляющем большинстве случаев мы будем хотеть, чтобы модель ответа лишь фильтровала/удаляла некоторые данные из ответа, как в нашем примере.
И в таких случаях мы можем использовать классы и наследование, чтобы пользоваться преимуществами аннотаций типов и получать более полную статическую проверку типов. Но при этом все так же получать фильтрацию ответа от FastAPI.
Для начала давайте рассмотрим как наш редактор кода, mypy и другие помощники разработчика видят аннотации типов.
У модели BaseUser есть некоторые поля. Затем UserIn наследуется от BaseUser и добавляет новое поле password. Таким образом модель будет включать в себя все поля из первой модели (родителя), а также свои собственные.
Мы аннотируем возвращаемый тип функции как BaseUser, но фактически мы будем возвращать объект типа UserIn.
Редакторы, mypy и другие инструменты не будут иметь возражений против такого подхода, поскольку UserIn является подклассом BaseUser. Это означает, что такой тип будет корректным, т.к. ответ может быть чем угодно, если это будет BaseUser.
FastAPI знает тип ответа функции, так что вы можете быть уверены, что на выходе будут только те поля, которые вы указали.
FastAPI совместно с Pydantic выполнит некоторую магию "под капотом", чтобы убедиться, что те же самые правила наследования классов не используются для фильтрации возвращаемых данных, в противном случае вы могли бы в конечном итоге вернуть гораздо больше данных, чем ожидали.
Таким образом, вы можете получить все самое лучшее из обоих миров: аннотации типов с поддержкой инструментов для разработки и фильтрацию данных.
Если посмотреть на сгенерированную документацию, вы можете убедиться, что в ней присутствуют обе JSON схемы - как для входной модели, так и для выходной:
И также обе модели будут использованы в интерактивной документации API:
Бывают случаи, когда вы возвращаете что-то, что не является валидным типом для Pydantic и вы указываете аннотацию ответа функции только для того, чтобы работала поддержка различных инструментов (редактор кода, mypy и др.).
fromfastapiimportFastAPI,Responsefromfastapi.responsesimportJSONResponse,RedirectResponseapp=FastAPI()@app.get("/portal")asyncdefget_portal(teleport:bool=False)->Response:ifteleport:returnRedirectResponse(url="https://www.youtube.com/watch?v=dQw4w9WgXcQ")returnJSONResponse(content={"message":"Here's your interdimensional portal."})
Это поддерживается FastAPI по-умолчанию, т.к. аннотация проставлена в классе (или подклассе) Response.
И ваши помощники разработки также будут счастливы, т.к. оба класса RedirectResponse и JSONResponse являются подклассами Response. Таким образом мы получаем корректную аннотацию типа.
Но когда вы возвращаете какой-либо другой произвольный объект, который не является допустимым типом Pydantic (например, объект из базы данных), и вы аннотируете его подобным образом для функции, FastAPI попытается создать из этого типа модель Pydantic и потерпит неудачу.
То же самое произошло бы, если бы у вас было что-то вроде Union различных типов и один или несколько из них не являлись бы допустимыми типами для Pydantic. Например, такой вариант приведет к ошибке 💥:
fromfastapiimportFastAPI,Responsefromfastapi.responsesimportRedirectResponseapp=FastAPI()@app.get("/portal")asyncdefget_portal(teleport:bool=False)->Response|dict:ifteleport:returnRedirectResponse(url="https://www.youtube.com/watch?v=dQw4w9WgXcQ")return{"message":"Here's your interdimensional portal."}
fromtypingimportUnionfromfastapiimportFastAPI,Responsefromfastapi.responsesimportRedirectResponseapp=FastAPI()@app.get("/portal")asyncdefget_portal(teleport:bool=False)->Union[Response,dict]:ifteleport:returnRedirectResponse(url="https://www.youtube.com/watch?v=dQw4w9WgXcQ")return{"message":"Here's your interdimensional portal."}
...такой код вызовет ошибку, потому что в аннотации указан неподдерживаемый Pydantic тип. А также этот тип не является классом или подклассом Response.
Продолжим рассматривать предыдущий пример. Допустим, что вы хотите отказаться от автоматической валидации ответа, документации, фильтрации и т.д.
Но в то же время, хотите сохранить аннотацию возвращаемого типа для функции, чтобы обеспечить работу помощников и анализаторов типов (например, mypy).
В таком случае, вы можете отключить генерацию модели ответа, указав response_model=None:
fromfastapiimportFastAPI,Responsefromfastapi.responsesimportRedirectResponseapp=FastAPI()@app.get("/portal",response_model=None)asyncdefget_portal(teleport:bool=False)->Response|dict:ifteleport:returnRedirectResponse(url="https://www.youtube.com/watch?v=dQw4w9WgXcQ")return{"message":"Here's your interdimensional portal."}
fromtypingimportUnionfromfastapiimportFastAPI,Responsefromfastapi.responsesimportRedirectResponseapp=FastAPI()@app.get("/portal",response_model=None)asyncdefget_portal(teleport:bool=False)->Union[Response,dict]:ifteleport:returnRedirectResponse(url="https://www.youtube.com/watch?v=dQw4w9WgXcQ")return{"message":"Here's your interdimensional portal."}
Тогда FastAPI не станет генерировать модель ответа и вы сможете сохранить такую аннотацию типа, которая вам требуется, никак не влияя на работу FastAPI. 🤓
description: Union[str, None] = None (или str | None = None в Python 3.10), где None является значением по умолчанию.
tax: float = 10.5, где 10.5 является значением по умолчанию.
tags: List[str] = [], где пустой список [] является значением по умолчанию.
но вы, возможно, хотели бы исключить их из ответа, если данные поля не были заданы явно.
Например, у вас есть модель с множеством необязательных полей в NoSQL базе данных, но вы не хотите отправлять в качестве ответа очень длинный JSON с множеством значений по умолчанию.
Если значение поля совпадает с его значением по умолчанию¶
Если данные содержат те же значения, которые являются для этих полей по умолчанию, но были установлены явно - как для элемента с ID = baz, ответ будет таким:
FastAPI достаточно умен (на самом деле, это заслуга Pydantic), чтобы понять, что, хотя description, tax и tags хранят такие же данные, какие должны быть по умолчанию - для них эти значения были установлены явно (а не получены из значений по умолчанию).
И поэтому, они также будут включены в JSON ответа.
Подсказка
Значением по умолчанию может быть что угодно, не только None.
Им может быть и список ([]), значение 10.5 типа float, и т.п.
Вы также можете использовать параметры декоратора операции пути, такие, как response_model_include и response_model_exclude.
Они принимают аргументы типа set, состоящий из строк (str) с названиями атрибутов, которые либо требуется включить в ответ (при этом исключив все остальные), либо наоборот исключить (оставив в ответе все остальные поля).
Это можно использовать как быстрый способ исключить данные из ответа, не создавая отдельную модель Pydantic.
Подсказка
Но по-прежнему рекомендуется следовать изложенным выше советам и использовать несколько моделей вместо данных параметров.
Потому как JSON схема OpenAPI, генерируемая вашим приложением (а также документация) все еще будет содержать все поля, даже если вы использовали response_model_include или response_model_exclude и исключили некоторые атрибуты.
То же самое применимо к параметру response_model_by_alias.
fromfastapiimportFastAPIfrompydanticimportBaseModelapp=FastAPI()classItem(BaseModel):name:strdescription:str|None=Noneprice:floattax:float=10.5items={"foo":{"name":"Foo","price":50.2},"bar":{"name":"Bar","description":"The Bar fighters","price":62,"tax":20.2},"baz":{"name":"Baz","description":"There goes my baz","price":50.2,"tax":10.5,},}@app.get("/items/{item_id}/name",response_model=Item,response_model_include={"name","description"},)asyncdefread_item_name(item_id:str):returnitems[item_id]@app.get("/items/{item_id}/public",response_model=Item,response_model_exclude={"tax"})asyncdefread_item_public_data(item_id:str):returnitems[item_id]
fromtypingimportUnionfromfastapiimportFastAPIfrompydanticimportBaseModelapp=FastAPI()classItem(BaseModel):name:strdescription:Union[str,None]=Noneprice:floattax:float=10.5items={"foo":{"name":"Foo","price":50.2},"bar":{"name":"Bar","description":"The Bar fighters","price":62,"tax":20.2},"baz":{"name":"Baz","description":"There goes my baz","price":50.2,"tax":10.5,},}@app.get("/items/{item_id}/name",response_model=Item,response_model_include={"name","description"},)asyncdefread_item_name(item_id:str):returnitems[item_id]@app.get("/items/{item_id}/public",response_model=Item,response_model_exclude={"tax"})asyncdefread_item_public_data(item_id:str):returnitems[item_id]
Подсказка
При помощи кода {"name","description"} создается объект множества (set) с двумя строковыми значениями.
Того же самого можно достичь используя set(["name", "description"]).
Если вы забыли про set и использовали структуру list или tuple, FastAPI автоматически преобразует этот объект в set, чтобы обеспечить корректную работу:
fromfastapiimportFastAPIfrompydanticimportBaseModelapp=FastAPI()classItem(BaseModel):name:strdescription:str|None=Noneprice:floattax:float=10.5items={"foo":{"name":"Foo","price":50.2},"bar":{"name":"Bar","description":"The Bar fighters","price":62,"tax":20.2},"baz":{"name":"Baz","description":"There goes my baz","price":50.2,"tax":10.5,},}@app.get("/items/{item_id}/name",response_model=Item,response_model_include=["name","description"],)asyncdefread_item_name(item_id:str):returnitems[item_id]@app.get("/items/{item_id}/public",response_model=Item,response_model_exclude=["tax"])asyncdefread_item_public_data(item_id:str):returnitems[item_id]
fromtypingimportUnionfromfastapiimportFastAPIfrompydanticimportBaseModelapp=FastAPI()classItem(BaseModel):name:strdescription:Union[str,None]=Noneprice:floattax:float=10.5items={"foo":{"name":"Foo","price":50.2},"bar":{"name":"Bar","description":"The Bar fighters","price":62,"tax":20.2},"baz":{"name":"Baz","description":"There goes my baz","price":50.2,"tax":10.5,},}@app.get("/items/{item_id}/name",response_model=Item,response_model_include=["name","description"],)asyncdefread_item_name(item_id:str):returnitems[item_id]@app.get("/items/{item_id}/public",response_model=Item,response_model_exclude=["tax"])asyncdefread_item_public_data(item_id:str):returnitems[item_id]
Используйте параметр response_model у декоратора операции пути для того, чтобы задать модель ответа и в большей степени для того, чтобы быть уверенным, что приватная информация будет отфильтрована.
А также используйте response_model_exclude_unset, чтобы возвращать только те значения, которые были заданы явно.