Skip to content

Commit

Permalink
✨ Enable Pydantic's serialization mode for responses, add support for…
Browse files Browse the repository at this point in the history
… Pydantic's `computed_field`, better OpenAPI for response models, proper required attributes, better generated clients (#10011)

* ✨ Enable Pydantic's serialization mode for responses

* ✅ Update tests with new Pydantic v2 serialization mode

* ✅ Add a test for Pydantic v2's computed_field
  • Loading branch information
tiangolo committed Aug 4, 2023
1 parent d943e02 commit 19a2c3b
Show file tree
Hide file tree
Showing 31 changed files with 1,446 additions and 256 deletions.
4 changes: 1 addition & 3 deletions fastapi/routing.py
Original file line number Diff line number Diff line change
Expand Up @@ -448,9 +448,7 @@ def __init__(
self.response_field = create_response_field(
name=response_name,
type_=self.response_model,
# TODO: This should actually set mode='serialization', just, that changes the schemas
# mode="serialization",
mode="validation",
mode="serialization",
)
# Create a clone of the field, so that a Pydantic submodel is not returned
# as is just because it's an instance of a subclass of a more limited class
Expand Down
77 changes: 77 additions & 0 deletions tests/test_computed_fields.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,77 @@
import pytest
from fastapi import FastAPI
from fastapi.testclient import TestClient

from .utils import needs_pydanticv2


@pytest.fixture(name="client")
def get_client():
app = FastAPI()

from pydantic import BaseModel, computed_field

class Rectangle(BaseModel):
width: int
length: int

@computed_field
@property
def area(self) -> int:
return self.width * self.length

@app.get("/")
def read_root() -> Rectangle:
return Rectangle(width=3, length=4)

client = TestClient(app)
return client


@needs_pydanticv2
def test_get(client: TestClient):
response = client.get("/")
assert response.status_code == 200, response.text
assert response.json() == {"width": 3, "length": 4, "area": 12}


@needs_pydanticv2
def test_openapi_schema(client: TestClient):
response = client.get("/openapi.json")
assert response.status_code == 200, response.text
assert response.json() == {
"openapi": "3.1.0",
"info": {"title": "FastAPI", "version": "0.1.0"},
"paths": {
"/": {
"get": {
"summary": "Read Root",
"operationId": "read_root__get",
"responses": {
"200": {
"description": "Successful Response",
"content": {
"application/json": {
"schema": {"$ref": "#/components/schemas/Rectangle"}
}
},
}
},
}
}
},
"components": {
"schemas": {
"Rectangle": {
"properties": {
"width": {"type": "integer", "title": "Width"},
"length": {"type": "integer", "title": "Length"},
"area": {"type": "integer", "title": "Area", "readOnly": True},
},
"type": "object",
"required": ["width", "length", "area"],
"title": "Rectangle",
}
}
},
}
8 changes: 6 additions & 2 deletions tests/test_filter_pydantic_sub_model_pv2.py
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
from typing import Optional

import pytest
from dirty_equals import HasRepr, IsDict
from dirty_equals import HasRepr, IsDict, IsOneOf
from fastapi import Depends, FastAPI
from fastapi.exceptions import ResponseValidationError
from fastapi.testclient import TestClient
Expand Down Expand Up @@ -139,7 +139,11 @@ def test_openapi_schema(client: TestClient):
},
"ModelA": {
"title": "ModelA",
"required": ["name", "foo"],
"required": IsOneOf(
["name", "description", "foo"],
# TODO remove when deprecating Pydantic v1
["name", "foo"],
),
"type": "object",
"properties": {
"name": {"title": "Name", "type": "string"},
Expand Down
210 changes: 179 additions & 31 deletions tests/test_tutorial/test_body_updates/test_tutorial001.py
Original file line number Diff line number Diff line change
@@ -1,7 +1,8 @@
import pytest
from dirty_equals import IsDict
from fastapi.testclient import TestClient

from ...utils import needs_pydanticv1, needs_pydanticv2


@pytest.fixture(name="client")
def get_client():
Expand Down Expand Up @@ -36,7 +37,181 @@ def test_put(client: TestClient):
}


@needs_pydanticv2
def test_openapi_schema(client: TestClient):
response = client.get("/openapi.json")
assert response.status_code == 200, response.text
assert response.json() == {
"openapi": "3.1.0",
"info": {"title": "FastAPI", "version": "0.1.0"},
"paths": {
"/items/{item_id}": {
"get": {
"responses": {
"200": {
"description": "Successful Response",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/ItemOutput"
}
}
},
},
"422": {
"description": "Validation Error",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/HTTPValidationError"
}
}
},
},
},
"summary": "Read Item",
"operationId": "read_item_items__item_id__get",
"parameters": [
{
"required": True,
"schema": {"title": "Item Id", "type": "string"},
"name": "item_id",
"in": "path",
}
],
},
"put": {
"responses": {
"200": {
"description": "Successful Response",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/ItemOutput"
}
}
},
},
"422": {
"description": "Validation Error",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/HTTPValidationError"
}
}
},
},
},
"summary": "Update Item",
"operationId": "update_item_items__item_id__put",
"parameters": [
{
"required": True,
"schema": {"title": "Item Id", "type": "string"},
"name": "item_id",
"in": "path",
}
],
"requestBody": {
"content": {
"application/json": {
"schema": {"$ref": "#/components/schemas/ItemInput"}
}
},
"required": True,
},
},
}
},
"components": {
"schemas": {
"ItemInput": {
"title": "Item",
"type": "object",
"properties": {
"name": {
"title": "Name",
"anyOf": [{"type": "string"}, {"type": "null"}],
},
"description": {
"title": "Description",
"anyOf": [{"type": "string"}, {"type": "null"}],
},
"price": {
"title": "Price",
"anyOf": [{"type": "number"}, {"type": "null"}],
},
"tax": {"title": "Tax", "type": "number", "default": 10.5},
"tags": {
"title": "Tags",
"type": "array",
"items": {"type": "string"},
"default": [],
},
},
},
"ItemOutput": {
"title": "Item",
"type": "object",
"required": ["name", "description", "price", "tax", "tags"],
"properties": {
"name": {
"anyOf": [{"type": "string"}, {"type": "null"}],
"title": "Name",
},
"description": {
"anyOf": [{"type": "string"}, {"type": "null"}],
"title": "Description",
},
"price": {
"anyOf": [{"type": "number"}, {"type": "null"}],
"title": "Price",
},
"tax": {"title": "Tax", "type": "number", "default": 10.5},
"tags": {
"title": "Tags",
"type": "array",
"items": {"type": "string"},
"default": [],
},
},
},
"ValidationError": {
"title": "ValidationError",
"required": ["loc", "msg", "type"],
"type": "object",
"properties": {
"loc": {
"title": "Location",
"type": "array",
"items": {
"anyOf": [{"type": "string"}, {"type": "integer"}]
},
},
"msg": {"title": "Message", "type": "string"},
"type": {"title": "Error Type", "type": "string"},
},
},
"HTTPValidationError": {
"title": "HTTPValidationError",
"type": "object",
"properties": {
"detail": {
"title": "Detail",
"type": "array",
"items": {"$ref": "#/components/schemas/ValidationError"},
}
},
},
}
},
}


# TODO: remove when deprecating Pydantic v1
@needs_pydanticv1
def test_openapi_schema_pv1(client: TestClient):
response = client.get("/openapi.json")
assert response.status_code == 200, response.text
assert response.json() == {
Expand Down Expand Up @@ -124,36 +299,9 @@ def test_openapi_schema(client: TestClient):
"title": "Item",
"type": "object",
"properties": {
"name": IsDict(
{
"title": "Name",
"anyOf": [{"type": "string"}, {"type": "null"}],
}
)
| IsDict(
# TODO: remove when deprecating Pydantic v1
{"title": "Name", "type": "string"}
),
"description": IsDict(
{
"title": "Description",
"anyOf": [{"type": "string"}, {"type": "null"}],
}
)
| IsDict(
# TODO: remove when deprecating Pydantic v1
{"title": "Description", "type": "string"}
),
"price": IsDict(
{
"title": "Price",
"anyOf": [{"type": "number"}, {"type": "null"}],
}
)
| IsDict(
# TODO: remove when deprecating Pydantic v1
{"title": "Price", "type": "number"}
),
"name": {"title": "Name", "type": "string"},
"description": {"title": "Description", "type": "string"},
"price": {"title": "Price", "type": "number"},
"tax": {"title": "Tax", "type": "number", "default": 10.5},
"tags": {
"title": "Tags",
Expand Down
Loading

0 comments on commit 19a2c3b

Please sign in to comment.