简介

FastAPI,一个用于构建API的现代、快速(高性能)的web框架。
FsastAPI是建立在StarlettePydantic的基础上的。

  • Pydantic是一个基于Python类型提示来定义数据验证、序列化和文档的苦。
  • Starlette是一种轻量级的ASGI(Asynchronous Server Gateway Interface)框架/工具包,是构建高性能Asyncio服务的理性选择。
    依赖:Python3.6及更高的版本。
    • 我选择使用FastAPI的理由是,这个框架还支持WebSocket,而Flask和Django不支持

预备知识点

HTTP协议

  • 在这小节,我们来彻底弄清楚一下HTTP协议,带着下面这四个问题来看:

    1. 什么是请求头,请求体、响应头,响应体
    2. URL地址包括什么
    3. GET请求和POST请求到底是什么,有什么区别
    4. Content-Type是什么
  • 把这些彻底弄清楚之后,学习其他语言的Web框架就会容易很多

简介

  • 虽然上学时期都听腻了HTTP协议是Hyper Text Transfer Protocol(超文本传输协议)的缩写,是用于万维网(WWW:World Wide Web)服务器与本地浏览器之间传输超文本的传输协议。HTTP是一个属于应用层的面向对象的协议(为什么是应用层?建议回顾一下OSI七层模型或者TCP/IP四层模型)。

  • TCP/IP四层模型

层级 功能 示例协议
应用层 提供应用程序的接口,定义数据的语义。 HTTP, FTP, SMTP, DNS
传输层 提供端到端通信的可靠性和数据流管理。 TCP, UDP
网络层 负责路由和逻辑地址(IP 地址)的管理。 IP, ICMP
链路层 处理数据帧的传输及硬件地址(MAC 地址)。 Ethernet, Wi-Fi
  • OSI七层模型
层级 功能 示例协议
应用层 提供应用程序接口,支持网络应用程序的通信。 HTTP, FTP, SMTP, DNS, Telnet
表示层 负责数据的格式化、加密、解密及压缩处理。 JPEG, GIF, SSL/TLS, ASCII
会话层 管理应用程序之间的会话,包括建立、管理和终止。 NetBIOS, PPTP, RPC
传输层 提供端到端的通信,负责可靠性和数据流控制。 TCP, UDP
网络层 负责路由和逻辑地址管理,实现跨网络通信。 IP, ICMP, ARP, RIP, OSPF
数据链路层 处理数据帧的传输及物理地址(MAC地址)。 Ethernet, PPP, Wi-Fi, Frame Relay
物理层 负责比特流的传输,定义硬件电气及机械特性。 RS-232, DSL, ISDN, IEEE 802.11
  • 由于其简捷、快速的方式,适用于分布式超媒体信息系统。它于1990年提出,经过几年的使用和发展,得到不断的完善和扩展,HTTP协议工作于客户端-服务端架构上。浏览器作为HTTP客户端,通过URL向HTTP服务端即Web服务器发送所有请求。Web服务器根据接收到的请求,向客户端发送响应信息。

HTTP协议特性

基于TCP/IP协议

  1. 什么是基于TCP/IP

    • HTTP 依赖于 TCP 协议 来传输数据:
    • TCP 是传输层协议,提供可靠的、面向连接的通信。
    • HTTP 的每个请求和响应(例如 GET 请求)都通过 TCP 连接发送。
    • TCP 使用 IP 协议(网络层)来确定数据的目的地:
    • IP 负责将数据包路由到目标主机。
    • TCPIP 协作完成数据的分发和传输。
    • IP 数据包通过 链路层协议(如 EthernetWi-Fi)在实际的物理网络中传输。
  2. 具体的工作流程

    • 应用层(HTTP):用户在浏览器中输入网址(如www.baidu.com),浏览器通过HTTP发起请求。HTTP构建请求数据,调用传输层。
    • 传输层(TCP):HTTP请求糖果TCP建立一个连接(例如TCP三次握手)。TCP将数据分割为小的“段”,保证这些段可靠的传输。
    • 网络层(IP):TCP将数据包封装为IP数据包,通过IP协议将数据包通过路由器发送到目标主机。
    • 链路层(Ethernet):IP数据包通过链路层协议(如Ethernet或Wi-Fi)发送到目标主机。
    • 返回响应:目标服务器接受请求并通过相同的层次返回响应,直至用户浏览器完成解析。
  3. 总结

    • TCP 提供可靠的数据传输。
    • IP 负责数据路由。
      • HTTP 构建在这些基础之上,专注于实现 Web 通信的语义(如请求、响应、状态码等)。

基于请求-响应模式

  • HTTP协议规定,请求从客户端发出,最后服务器响应该请求并返回。换句话说,肯定是先从客户端开始建立通信的,服务端在没有接收到请求之前不会发送响应数据。
    • 请求和响应是HTTP协议的核心组成部分。
    • 请求(Request):客户端发送的请求数据,包括请求方法、URL、请求头、请求体等。
    • 响应(Response):服务器返回的响应数据,包括状态码、响应头、响应体等。

无状态保存

  • 在HTTP协议中,每次发送一个新的请求时,服务器都会杀横撑一个对应的新响应。这种请求-响应模式的设计中,协议不会记录之前的任何请求或响应内容。设计成无状态的主要目的是简化协议的实现,减少服务器的资源占用,从而能够快速处理大量事务。这种特性极大地提高了协议的可伸缩性,使其能够支持高并发和复杂的互联网应用场景。

  • 由于HTTP自身无法保存状态,因此需要借助其他技术(如Cookie和Session)来实现状态管理和数据持久化,从而满足实际应用中的需求。

  • 补充说明:
    1. Cookie是一种由服务器生成并存储在客户端浏览器中的小型数据,用于保存用户状态信息。例如登录会话或偏好设置,当用户再次访问同一网站时,浏览器会携带相应的Cookie信息,从而使浏览器能够识别用户身份或恢复之前的会话状态。
    2. Session是一种由服务器端维护的状态管理机制,通过在服务器端为每个用户创建唯一的会话标识符(Session ID),来保存用户的状态信息。与Cookie不同,Session ID通常存储在服务器端,而不是客户端。

短连接

  • HTTP1.0默认使用的是短连接。浏览器和服务器每进行一次HTTP操作,就建立一次连接,任务结束就中断连接。
    • 但是这种方式存在性能为问题,因为每次请求都需要进行三次握手、数据传输以及连接断开,这样会消耗额外的时间和资源。
  • HTTP/1.1引入了长连接(即复用TCP连接)。在默认情况下,HTTP/1.1会保持连接打开,允许在同一TCP连接上发送多个请求和响应。
    • Connection: keep-alive是HTTP/1.1中的一个请求/响应头,表示链接不关闭。但在实际使用中,HTTP/1.1的keep-alive连接默认会保持连接打开,即使没有显示指定keep-alive
    • 长连接的优势在于减少了重复建立和关闭连接的开销,提升了性能。
  • SSE和WebSocket
    • 上面说的长连接需要和SSE和WebSocket的持久连接区分一下。
    • SSE是基于HTTP协议的单向通信技术,通常用于服务器主动推送实时数据到客户端(如新闻推送、实时通知等)。SSE是一种持久的HTTP连接,服务器可以不断的向客户端发送数据。
    • WebSocket是一种更为灵活的双向通信协议(所以ws连接的url是ws://,而http连接的url是http://),客户端和服务器通过WebSocket建立持久连接后,双方可以随时互相发送数据,这种连接比SSE延迟更低,且实时性更强。WebSocket是独立于HTTP的,但是最初的握手是通过HTTP协议完成的。

HTTP请求协议与响应协议

  • HTTP协议包含有浏览器发送数据到服务器需要遵循的请求协议和服务器发送数据到浏览器需要遵循的请求协议。
  • 用户HTTP协议交互的信息被称为HTTP报文。请求端(客户端)的HTTP报文被称为请求报文,响应端(服务器)的HTTP报文被称为响应报文。
  • HTTP报文本身是有多行数据构成的文字文本,每行数据都是以\r\n结尾。

  • 一个完整的URL包括:协议、ip、端口、路径、参数
    • 例如:http://www.baidu.com:80/s?wd=python,其中http是协议,www.baidu.com是ip,443是端口,/s是路径,wd=python是参数。
    • 例如:ws://localhost:8000/ws,其中ws是协议,localhost是ip,8000是端口,/ws是路径。
  • GET和POST请求的区别
    • GET请求提交的数据会放在URL之后,以?分割URL和传输数据,参数之间以&相连。而POST请求则是把提交的数据放在HTTP请求报文的请求体中。
    • GET提交的数据大小有限制(因为浏览器对URL的长度有限制),而POST请求则没有此限制。

测试HTTP协议格式

  • 在这里,我们通过手写一个原生的 socket 程序来测试 HTTP 协议格式。常见的 Web 框架其实都是基于 socket 实现的,只不过框架已经封装了大部分细节,我们只需按照框架规则编写代码即可。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
import socket

sock = socket.socket()

sock.bind(("127.0.0.1", 9571))
sock.listen(5)
while True:
conn, addr = sock.accept()
data = conn.recv(1024)
decode_data = data.decode('utf-8')
print(f"接收到客户端发送的请求信息:{data}")
lines = decode_data.split('\r\n')
for line in lines:
print(line)
  • 当我们用浏览器访问http://127.0.0.1:9571/时,浏览器会发送一个GET请求,此时打印的结果是
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    接收到客户端发送的请求信息:b'GET / HTTP/1.1\r\nHost: localhost:9571\r\nConnection: keep-alive\r\nCache-Control: max-age=0\r\nsec-ch-ua: "Google Chrome";v="131", "Chromium";v="131", "Not_A Brand";v="24"\r\nsec-ch-ua-mobile: ?0\r\nsec-ch-ua-platform: "Windows"\r\nUpgrade-Insecure-Requests: 1\r\nUser-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/131.0.0.0 Safari/537.36\r\nAccept: text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,image/apng,*/*;q=0.8,application/signed-exchange;v=b3;q=0.7\r\nSec-Fetch-Site: none\r\nSec-Fetch-Mode: navigate\r\nSec-Fetch-User: ?1\r\nSec-Fetch-Dest: document\r\nAccept-Encoding: gzip, deflate, br, zstd\r\nAccept-Language: zh-CN,zh-TW;q=0.9,zh;q=0.8,en;q=0.7\r\nCookie: Hm_lvt_d54c3018e5f620d2cda0f5ca52ff80cb=1732428640,1734018506,1734100966,1734138834; HMACCOUNT=6EC0BD1C42ED9962; Hm_lpvt_d54c3018e5f620d2cda0f5ca52ff80cb=1734152185\r\n\r\n'
    ====================
    GET / HTTP/1.1
    Host: localhost:9571
    Connection: keep-alive
    Cache-Control: max-age=0
    sec-ch-ua: "Google Chrome";v="131", "Chromium";v="131", "Not_A Brand";v="24"
    sec-ch-ua-mobile: ?0
    sec-ch-ua-platform: "Windows"
    Upgrade-Insecure-Requests: 1
    User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/131.0.0.0 Safari/537.36
    Accept: text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,image/apng,*/*;q=0.8,application/signed-exchange;v=b3;q=0.7
    Sec-Fetch-Site: none
    Sec-Fetch-Mode: navigate
    Sec-Fetch-User: ?1
    Sec-Fetch-Dest: document
    Accept-Encoding: gzip, deflate, br, zstd
    Accept-Language: zh-CN,zh-TW;q=0.9,zh;q=0.8,en;q=0.7
    Cookie: Hm_lvt_d54c3018e5f620d2cda0f5ca52ff80cb=1732428640,1734018506,1734100966,1734138834; HMACCOUNT=6EC0BD1C42ED9962; Hm_lpvt_d54c3018e5f620d2cda0f5ca52ff80cb=1734152185


    ====================
  • 分析结果:
    1. 请求首行:如 GET / HTTP/1.1,表示请求方法是 GET,路径是 /,协议版本是 HTTP/1.1。
    2. 请求头:每行以 key: value 形式存在,例如 Host: localhost:9571 表示目标主机和端口。
    3. 空行:空行标识请求头结束。
    4. 请求体:如果有请求体,空行后会跟随内容,此处因无请求体而为空。
  • 在浏览器中没有显示任何内容的原因是:服务器未返回任何数据。即便返回数据,也必须严格按照 HTTP 协议格式。

  • 那我们现在来修改一下代码,使其有响应数据。

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    import socket

    sock = socket.socket()

    sock.bind(("127.0.0.1", 9571))
    sock.listen(5)
    while True:
    conn, addr = sock.accept()
    data = conn.recv(1024)
    decode_data = data.decode('utf-8')
    print(f"接收到客户端发送的请求信息:{data}")
    lines = decode_data.split('\r\n')
    print('=' * 20)
    for line in lines:
    print(line)
    print('=' * 20)

    conn.send(b'HTTP/1.1 200 OK\r\n\r\nHello World')
  • 浏览器收到响应后,正常显示 Hello World,因为我们遵循了 HTTP 协议格式,返回了以下内容:
    1. 响应首行:HTTP/1.1 200 OK,表示状态码为 200,状态为 OK。
    2. 空行:标识响应头结束。
    3. 响应体:Hello World。

测试Content-Type

  • 在响应体中我们可以返回各种数据,但是浏览器怎么知道返回的数据是什么类型的呢?这就需要用到Content-Type
  • Content-Type是HTTP协议中用于描述响应体数据类型的头部字段。例如:
    • Content-Type: text/html,表示响应体是HTML格式的数据。
    • Content-Type: application/json,表示响应体是JSON格式的数据。
    • Content-Type: text/plain,表示响应体是纯文本格式的数据。
    • Content-Type: image/jpeg,表示响应体是JPEG格式的图片。
    • Content-Type: application/octet-stream,表示响应体是二进制流数据。

请求时的Content-Type

  • 以下是用 Apifox 工具分别发送不同类型请求时的表现:
    1. GET 请求
      • 示例 URL:http://localhost:9571?name=kyle&age=21
      • 参数放在请求首行:
        1
        2
        3
        4
        5
        6
        7
        8
        9
        10
        11
        接收到客户端发送的请求信息:b'GET /?name=kyle&age=21 HTTP/1.1\r\nUser-Agent: Apifox/1.0.0 (https://apifox.com)\r\nAccept: */*\r\nHost: localhost:9571\r\nAccept-Encoding: gzip, deflate, br\r\nConnection: keep-alive\r\n\r\n'
        ====================
        GET /?name=kyle&age=21 HTTP/1.1
        User-Agent: Apifox/1.0.0 (https://apifox.com)
        Accept: */*
        Host: localhost:9571
        Accept-Encoding: gzip, deflate, br
        Connection: keep-alive


        ====================
    2. POST 请求(application/x-www-form-urlencoded)
      • 参数以 key=value 格式放在请求体中:
        1
        2
        3
        4
        5
        6
        7
        8
        9
        10
        11
        12
        13
        接收到客户端发送的请求信息:b'POST / HTTP/1.1\r\nUser-Agent: Apifox/1.0.0 (https://apifox.com)\r\nAccept: */*\r\nHost: localhost:9571\r\nAccept-Encoding: gzip, deflate, br\r\nConnection: keep-alive\r\nContent-Type: application/x-www-form-urlencoded\r\nContent-Length: 16\r\n\r\nname=kyle&age=21'
        ====================
        POST / HTTP/1.1
        User-Agent: Apifox/1.0.0 (https://apifox.com)
        Accept: */*
        Host: localhost:9571
        Accept-Encoding: gzip, deflate, br
        Connection: keep-alive
        Content-Type: application/x-www-form-urlencoded
        Content-Length: 16

        name=kyle&age=21
        ====================
    3. POST 请求(application/json)
      • 参数为 JSON 格式:
        1
        2
        3
        4
        5
        6
        7
        8
        9
        10
        11
        12
        13
        接收到客户端发送的请求信息:b'POST / HTTP/1.1\r\nUser-Agent: Apifox/1.0.0 (https://apifox.com)\r\nContent-Type: application/json\r\nAccept: */*\r\nHost: localhost:9571\r\nAccept-Encoding: gzip, deflate, br\r\nConnection: keep-alive\r\nContent-Length: 27\r\n\r\n{"name": "kyle", "age": 21}'
        ====================
        POST / HTTP/1.1
        User-Agent: Apifox/1.0.0 (https://apifox.com)
        Content-Type: application/json
        Accept: */*
        Host: localhost:9571
        Accept-Encoding: gzip, deflate, br
        Connection: keep-alive
        Content-Length: 27

        {"name": "kyle", "age": 21}
        ====================

响应时的Content-Type

  • 如果服务器返回的 Content-Type 不正确,浏览器可能无法正确解析响应体。例如:
    1. 错误的 Content-Type,以下代码返回了 HTML 内容,但设置了 Content-Type: text/plain:
      1
      conn.send(b'HTTP/1.1 200 OK\r\nContent-Type: text/plain\r\n\r\n<h1>Hello World</h1>')
      浏览器中惠当做纯文本显示
      1
      <h1>Hello World</h1>
    2. 正确的 Content-Type,将 Content-Type 改为 text/html
      1
      conn.send(b'HTTP/1.1 200 OK\r\nContent-Type: text/html\r\n\r\n<h1>Hello World</h1>')
      浏览器中会正确显示 HTML 内容:
      1
      Hello World

快速开始

  • 安装fastapi和uvicorn库
    1
    2
    pip install fastapi
    pip install uvicorn
  • 创建一个main.py文件,并编写以下代码:
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    from fastapi import FastAPI
    import uvicorn

    app = FastAPI()


    @app.get('/test')
    def first_api():
    return {}


    if __name__ == '__main__':
    uvicorn.run('main:app', host='127.0.0.1', port=9421, reload=True)
  • fastapi还内置了Swagger UI,可以通过http://127.0.0.1:9421/docs 访问。

路径操作

  • fastapi支持各种请求方式,但是实际使用中,建议只使用get和post方式。
    1
    2
    3
    4
    5
    6
    7
    8
    @app.get()
    @app.post()
    @app.put()
    @app.patch()
    @app.delete()
    @app.options()
    @app.head()
    @app.trace()

路径操作做装饰器参数

  • 这部分和Java里的Swagger注解类似,可以用来描述API的元数据。
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    from typing import Optional
    from fastapi import FastAPI
    import uvicorn
    from pydantic import BaseModel
    from starlette import status

    app = FastAPI()


    class Item(BaseModel):
    name: str
    description: Optional[str] = None
    price: float
    tax: Optional[float] = None


    @app.post(
    "/items/{item_id}", # 路径模板
    response_model=Item, # 响应模型
    status_code=status.HTTP_201_CREATED, # 创建成功的状态码
    tags=["Items"], # 分类标签
    summary="创建一个新的项目", # 简短摘要
    description="根据提供的信息创建一个新的项目实例。", # 详细描述
    response_description="新创建的项目的详细信息。" # 响应描述
    )
    def first_api():
    return {}


    if __name__ == '__main__':
    uvicorn.run('main:app', host='127.0.0.1', port=9421, reload=True)

  • 效果如下:

路由分发include_router

  • 在实际开发中,我们可能需要将不同的功能模块拆分到不同的文件中,此时就需要用到路由分发。
  • 路由分发需要使用include_router方法,该方法接收一个APIRouter对象,并将其添加到FastAPI应用中。
    • 定义订单相关的路由
      1
      2
      3
      4
      5
      6
      7
      8
      9
      10
      from fastapi import APIRouter

      order_route = APIRouter()


      @order_route.get('/get_order_info')
      def get_order_info():
      return {
      'order_id': 123
      }

    定义用户相关的路由

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    from fastapi import APIRouter

    user_route = APIRouter()


    @user_route.get('/get_user_info')
    def get_user_info():
    return {
    'user_id': 123,
    'username': 'Lucy'
    }

    • 在main.py中引入路由分发,其中prefix参数用于指定路由的前缀,tags参数用于指定路由的标签,将在Swagger UI中显示。
      1
      2
      3
      4
      5
      6
      7
      8
      9
      10
      11
      12
      from fastapi import FastAPI
      import uvicorn
      from order.order_service import order_route
      from user.user_service import user_route

      app = FastAPI()

      app.include_router(user_route, prefix='/user', tags=['用户相关接口'])
      app.include_router(order_route, prefix='/order', tags=['订单相关接口'])

      if __name__ == '__main__':
      uvicorn.run('main:app', host='127.0.0.1', port=9421, reload=True)
    • 其中目录结构如图

    • 效果如下:

请求与响应

路径参数

基本使用

  • 路径参数是指在URL中定义的参数,例如:/get_user_info/{user_id},其中{user_id}就是路径参数,同时路径参数会作为函数的参数传入。
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    from fastapi import APIRouter

    user_route = APIRouter()


    @user_route.get('/get_user_info/{user_id}')
    def get_user_info(user_id):
    return {
    'user_id': user_id,
    'username': 'Lucy'
    }
  • 当我们访问http://127.0.0.1:9421/get_user_info/123时,会返回{'user_id': 123, 'username': 'Lucy'}

类型注解

  • 在刚刚的代码中,路径参数user_id没有指定类型注解,所以甚至可以传入字符串作为参数。当我们访问http://127.0.0.1:9421/user/get_user_info/qwe时,会返回{'user_id': 'qwe', 'username': 'Lucy'}
  • 在实际开发中,我们大部分情况想回希望路径参数的类型是固定的,所以需要指定类型注解,例如:
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    from fastapi import APIRouter

    user_route = APIRouter()


    @user_route.get('/get_user_info/{user_id}')
    def get_user_info(user_id: int):
    return {
    'user_id': user_id,
    'username': 'Lucy'
    }
  • 此时当我们访问http://127.0.0.1:9421/user/get_user_info/qwe时,就会报错
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    {
    "detail": [
    {
    "type": "int_parsing",
    "loc": [
    "path",
    "user_id"
    ],
    "msg": "Input should be a valid integer, unable to parse string as an integer",
    "input": "qwe"
    }
    ]
    }

查询参数(Query Parameters)

  • 查询参数是指在URL中定义的参数,例如:/get_user_info?user_id=123,其中user_id就是查询参数,同时查询参数会作为函数的参数传入。
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    from fastapi import APIRouter

    user_route = APIRouter()


    @user_route.get('/get_user_info')
    def get_user_info(user_id: int):
    return {
    'user_id': user_id,
    'username': 'Lucy'
    }
  • 当我们访问http://127.0.0.1:9421/user/get_user_info?user_id=123时,会返回{'user_id': 123, 'username': 'Lucy'}

  • 自python3.5开始,PEP484引入了类型注解(Type Hints),用于在函数参数和返回值中指定类型。作用主要有:

    • 提高代码的可读性和可维护性。
    • 在运行时进行类型检查,避免运行时错误。
    • 在IDE中提供代码补全和错误检查功能。
    • 在静态类型检查工具中进行类型检查,避免运行时错误。
    • 在代码文档中提供类型信息,方便其他开发者理解代码。
      Union是当参数类型不确定时,可以使用Union类型注解,例如:
      1
      2
      3
      4
      5
      6
      7
      from typing import Union

      def get_user_info(user_id: Union[int, str]):
      return {
      'user_id': user_id,
      'username': 'Lucy'
      }
      OptionalUnion的一个简化,表示参数可以是None或指定类型,不需要显示的指定NoneOptional[int]相当于Union[int, None]
      1
      2
      3
      4
      5
      6
      7
      from typing import Optional

      def get_user_info(user_id: Optional[int] = None):
      return {
      'user_id': user_id,
      'username': 'Lucy'
      }

请求体数据

  • 当你需要将数据从客户端发送给服务器时,就需要使用请求体数据。
  • FastAPI基于pydantic库,而pydantic库主要用来做类型强制检查,对于API服务,支持类型检查非常有用,会让服务更加健壮,也会加快开发速度,因为不用随时随地都要检查一下数据类型是否符合业务需求。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
from fastapi import APIRouter
from typing import List, Optional
from datetime import date

from pydantic import BaseModel

user_route = APIRouter()


class Addr(BaseModel):
city: str = None
street: str = None


class User(BaseModel):
name: str = None
age: int = None
birth: Optional[date] = None
friends: List[int] = None
addr: Addr = None


class Data(BaseModel):
user_list: List[User] = None


@user_route.post('/get_user_info')
def get_user_info(data: Data):
return data.user_list

  • POST方式请求url:http://127.0.0.1:9421/user/get_user_info,请求体数据:
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    {
    "user_list": [
    {
    "name": "Alice",
    "age": 30,
    "birth": "1994-05-20",
    "friends": [
    1,
    2
    ],
    "addr": {
    "city": "Alicetown",
    "street": "Tea Party Lane"
    }
    },
    {
    "name": "David",
    "age": 40,
    "birth": "1984-01-01",
    "friends": [
    1
    ],
    "addr": {
    "city": "Davidsville",
    "street": "Invisible Cloak Road"
    }
    }
    ]
    }
  • 返回结果:
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    [
    {
    "name": "Alice",
    "age": 30,
    "birth": "1994-05-20",
    "friends": [
    1,
    2
    ],
    "addr": {
    "city": "Alicetown",
    "street": "Tea Party Lane"
    }
    },
    {
    "name": "David",
    "age": 40,
    "birth": "1984-01-01",
    "friends": [
    1
    ],
    "addr": {
    "city": "Davidsville",
    "street": "Invisible Cloak Road"
    }
    }
    ]

Field

  • Pydantic中,Field方法还可以指定额外的验证规则和默认值,例如
    1
    2
    3
    4
    5
    6
    class User(BaseModel):
    name: str = None,
    age: int = Field(default=0, gt=0, lt=100),
    birth: Optional[date] = None,
    friends: List[int] = None,
    addr: Addr = None
  • 关于 Field 中的参数:
    1. default=0: 设置了 age 字段的默认值为 0。如果创建 User 实例时没有提供 age 的值,则它将被设置为 0。
    2. lt=100: lt 表示 less than(小于),这意味着 age 的值必须小于 100。
    3. gt=0: gt 表示 greater than(大于),这意味着 age 的值必须大于 0。
  • 当我们输出一个不符合范围的age时,就会报错
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    {
    "detail": [
    {
    "type": "less_than",
    "loc": [
    "body",
    "user_list",
    0,
    "age"
    ],
    "msg": "Input should be less than 100",
    "input": 200,
    "ctx": {
    "lt": 100
    }
    },
    {
    "type": "greater_than",
    "loc": [
    "body",
    "user_list",
    1,
    "age"
    ],
    "msg": "Input should be greater than 0",
    "input": -1,
    "ctx": {
    "gt": 0
    }
    }
    ]
    }

自定义验证器

  • Pydantic还支持自定义验证器来处理特定字段或者整个模型的数据验证,允许执行更复杂的逻辑检查,而不仅仅是简单的类型检查和范围限制。
  • Pydantic提供了@validator装饰器和@root_validator装饰器来处理自定义验证逻辑。
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    class User(BaseModel):
    name: str = None
    age: int = Field(default=0, lt=100, gt=0)
    birth: Optional[date] = None
    friends: List[int] = None
    addr: Addr = None

    @validator('name')
    def check_name(cls, v):
    if v is not None and v: # 检查是否为非空字符串
    if not v[0].isupper(): # 正确的方法名
    raise ValueError('姓名必须是大写字母开头')
    return v
  • 当我们输出一个不符合条件的name时
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    {
    "user_list": [
    {
    "name": "alice",
    "age": 80,
    "birth": "1994-05-20",
    "friends": [
    1,
    2
    ],
    "addr": {
    "city": "Alicetown",
    "street": "Tea Party Lane"
    }
    }
    ]
    }
  • 结果会报错
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    {
    "detail": [
    {
    "type": "value_error",
    "loc": [
    "body",
    "user_list",
    0,
    "name"
    ],
    "msg": "Value error, 姓名必须是大写字母开头",
    "input": "alice",
    "ctx": {
    "error": {}
    }
    }
    ]
    }