diff --git a/docs/tutorial/extra-models.md b/docs/tutorial/extra-models.md new file mode 100644 index 000000000..1e24c7478 --- /dev/null +++ b/docs/tutorial/extra-models.md @@ -0,0 +1,45 @@ +Continuing with the previous example, it will be common to have more than one related model. + +This is especially the case for user models, because: + +* The **input model** needs to be able to have a password +* The **output model** should do not have a password +* The **database model** would probably need to have a hashed password + +!!! danger + Never store user's plaintext passwords. Always store a secure hash that you can then verify. + +## Multiple models + +Here's a general idea of how the models could look like with their password fields and the places where they are used: + +```Python hl_lines="8 10 15 21 23 32 34 39 40" +{!./tutorial/src/extra-models/tutorial001.py!} +``` + +!!! warning + The supporting additional functions are just to demo a possible flow of the data, but they of course are not providing any real security. + +## Reduce duplication + +Reducing code duplication is one of the core ideas in **FastAPI**. + +As code duplication increments the chances of bugs, security issues, code desynchronization issues (when you update in one place but not in the others), etc. + +And these models are all sharing a lot of the data and duplicating attribute names and types. + +We could do better. + +We can declare a `Userbase` model that serves as a base for our other models. And then we can make subclasses of that model that inherit its attributes (type declarations, validation, etc). + +All the data conversion, validation, documentation, etc. will still work as normally. + +That way, we can declare just the differences between the models (with plaintext `password`, with `hashed_password` and without password): + +```Python hl_lines="8 14 15 18 19 22 23" +{!./tutorial/src/extra-models/tutorial002.py!} +``` + +## Recap + +Use multiple Pydantic models and inherit freely for each case. You don't need to have a single data model per entity if that entity must be able to have different "states". As the case with the user "entity" with a state including `password`, `password_hash` and no password. \ No newline at end of file diff --git a/docs/tutorial/src/extra-models/tutorial001.py b/docs/tutorial/src/extra-models/tutorial001.py new file mode 100644 index 000000000..aa8e7dad4 --- /dev/null +++ b/docs/tutorial/src/extra-models/tutorial001.py @@ -0,0 +1,42 @@ +from fastapi import FastAPI +from pydantic import BaseModel +from pydantic.types import EmailStr + +app = FastAPI() + + +class UserIn(BaseModel): + username: str + password: str + email: EmailStr + full_name: str = None + + +class UserOut(BaseModel): + username: str + email: EmailStr + full_name: str = None + + +class UserInDB(BaseModel): + username: str + hashed_password: str + email: EmailStr + full_name: str = None + + +def fake_password_hasher(raw_password: str): + return "supersecret" + raw_password + + +def fake_save_user(user_in: UserIn): + hashed_password = fake_password_hasher(user_in.password) + user_in_db = UserInDB(**user_in.dict(), hashed_password=hashed_password) + print("User saved! ..not really") + return user_in_db + + +@app.post("/user/", response_model=UserOut) +async def create_user(*, user_in: UserIn): + user_saved = fake_save_user(user_in) + return user_saved diff --git a/docs/tutorial/src/extra-models/tutorial002.py b/docs/tutorial/src/extra-models/tutorial002.py new file mode 100644 index 000000000..605baf91f --- /dev/null +++ b/docs/tutorial/src/extra-models/tutorial002.py @@ -0,0 +1,40 @@ +from fastapi import FastAPI +from pydantic import BaseModel +from pydantic.types import EmailStr + +app = FastAPI() + + +class UserBase(BaseModel): + username: str + email: EmailStr + full_name: str = None + + +class UserIn(UserBase): + password: str + + +class UserOut(UserBase): + pass + + +class UserInDB(UserBase): + hashed_password: str + + +def fake_password_hasher(raw_password: str): + return "supersecret" + raw_password + + +def fake_save_user(user_in: UserIn): + hashed_password = fake_password_hasher(user_in.password) + user_in_db = UserInDB(**user_in.dict(), hashed_password=hashed_password) + print("User saved! ..not really") + return user_in_db + + +@app.post("/user/", response_model=UserOut) +async def create_user(*, user_in: UserIn): + user_saved = fake_save_user(user_in) + return user_saved diff --git a/mkdocs.yml b/mkdocs.yml index 7770d5fe5..1fc0ce5d1 100644 --- a/mkdocs.yml +++ b/mkdocs.yml @@ -30,6 +30,7 @@ nav: - Cookie Parameters: 'tutorial/cookie-params.md' - Header Parameters: 'tutorial/header-params.md' - Response Model: 'tutorial/response-model.md' + - Extra Models: 'tutorial/extra-models.md' - Concurrency and async / await: 'async.md' - Deployment: 'deployment.md'