Union 联合类型 用法详解

Union 表示一个变量/参数/返回值可以是多种类型中的任意一种,主流用于 Python 类型注解,同时补充 TypeScript 等场景,重点讲 Python。


一、Python 中的 Union

环境说明

  • Python 3.10+:支持竖线简写 类型1 | 类型2
  • Python 3.9 及更早:需要从 typing 导入 Union

1. 基础语法

方式1:传统写法(兼容全版本)
from typing import Union

# 变量可以是 int 或 str
a: Union[int, str] = 10
a = "hello"

# 函数参数:接收 int / float
def add(x: Union[int, float]) -> float:
    return x + 1.0
方式2:简写语法(Python 3.10+ 推荐)

无需导入,直接用 | 分隔类型:

# 等价于 Union[int, str]
b: int | str = 20
b = "test"

def calc(x: int | float) -> float:
    return x * 2

2. 多类型联合

支持两个以上类型

from typing import Union

# 可以是 int / str / None
val: Union[int, str, None] = None
val = 99
val = "abc"

# 3.10+ 简写
val2: int | str | None = 100

补充:None 常用组合 类型 | None,Python 也提供了别名 Optional[T]
Optional[int] = Union[int, None]

3. 结合容器类型

from typing import Union, List

# 列表元素:int 或 str
data: List[Union[int, str]] = [1, "two", 3]

# 简写版 (3.10+)
data2: list[int | str] = [4, "five"]

4. 函数返回值为 Union

from typing import Union

def get_info(flag: bool) -> Union[str, int]:
    if flag:
        return "success"
    else:
        return 0

二、常用搭配 & 实用技巧

1. Union + Optional(可空类型)

业务高频场景:参数允许传对应类型或空值

from typing import Optional

# 等价于 Union[str, None]
name: Optional[str] = None
name = "Tom"

# 3.10+ 直接写 str | None,更直观
age: int | None = 18

2. 类型判断(运行时校验)

isinstance() 判断当前实际类型:

def check_type(x: int | str) -> None:
    if isinstance(x, int):
        print("整数类型")
    elif isinstance(x, str):
        print("字符串类型")

check_type(123)
check_type("python")

3. 嵌套 Union

支持多层联合,逻辑一致:

from typing import Union

# 变量可以是 int / float / 列表
var: Union[int, float, list] = [1,2,3]

4. 联合类型与类型别名

类型多、重复使用时,用 TypeAlias 简化:

from typing import TypeAlias

NumType: TypeAlias = int | float
# 后续直接使用别名
def square(x: NumType) -> NumType:
    return x ** 2

三、易错点 & 注意事项

  1. Union 是「或」关系
    表示满足其中一种即可,不是同时为多个类型。

  2. Union 去重
    Union[int, int, str] 等价于 Union[int, str],重复类型会自动合并。

  3. 不要滥用 Union

    • 过多类型(如十几种)会降低代码可读性、破坏类型提示意义;
    • 优先考虑基类/抽象类、重载函数,而非无脑堆 Union。
  4. Any 的区别

    • Any:任意类型,关闭类型检查;
    • Union[A,B]限定只能是 A/B,依然有类型约束,优先用 Union。
  5. 旧版本 Python 限制
    3.9- 不支持 int | str 语法,必须导入 typing.Union


四、Pydantic 中 Union 实战(高频场景)

Pydantic 数据模型大量使用联合类型做参数校验:

from pydantic import BaseModel

class User(BaseModel):
    # id 可以是整数或字符串
    id: int | str
    # 可选字符串(允许为 None)
    nickname: str | None = None

u1 = User(id=1001)
u2 = User(id="u_1002", nickname="Jack")
print(u1)

五、拓展:TypeScript 联合类型(补充)

语法和 Python 简写版类似,用 |

// 变量可为 number 或 string
let num: number | string;
num = 10;
num = "10";

// 函数参数
function printVal(x: number | string) {
  console.log(x);
}

快速总结

  1. 作用:限定变量/参数属于多种类型之一
  2. Python 写法:
    • 旧版:from typing import Union + Union[T1, T2]
    • 3.10+ 推荐:T1 | T2 简写;
  3. Optional[T] = T | None,专门处理可空场景;
  4. 运行时用 isinstance() 判断实际类型;
  5. 多用于类型注解、接口参数、数据模型(如 Pydantic)。


JWT Token 超简明讲解(原理、结构、用法、优缺点)

JWT = JSON Web Token,是前后端无状态身份认证的主流方案,用来替代传统 Session。


一、核心作用

用户登录后,后端生成一段加密字符串返回给前端;
前端后续每次请求带上该字符串,后端不用查服务器存储,就能校验身份、解析用户信息。

核心特点:无状态 → 服务端不保存会话数据,天生适合分布式、集群、微服务。


二、JWT 整体结构

格式:三段字符串,用 . 分隔
Header.Payload.Signature

1. Header 头部(第一段)

Base64 编码的 JSON,存放算法 + 令牌类型

{
  "alg": "HS256",  // 签名算法:HMAC-SHA256 最常用
  "typ": "JWT"
}

2. Payload 载荷(第二段,核心数据

Base64 编码的 JSON,存放业务数据、声明
分为标准字段自定义字段

标准常用字段
  • sub:主题(一般存用户ID)
  • iss:签发方
  • exp过期时间戳(必用)
  • iat:签发时间
  • jti:令牌唯一ID
自定义字段

自己加:usernamerolephone非敏感信息

⚠️ 重点:Header 和 Payload 只是 Base64 编码,不是加密!可直接解码查看
绝对不要放:密码、密钥、银行卡等敏感数据。

3. Signature 签名(第三段,安全核心)

防止 Token 被篡改
生成规则:

签名 = HMAC-SHA256( Header.Base64 + "." + Payload.Base64, 服务端秘钥 )
  • 秘钥只有后端知道
  • 只要 Header / Payload 任意字符被修改,签名校验必然失败

三、完整登录 & 请求流程

  1. 用户登录:提交账号密码 → 后端校验
  2. 生成 JWT:校验通过,组装 Header + Payload,用秘钥签名,返回完整 JWT
  3. 前端存储:一般存在 localStorage / Cookie
  4. 后续接口请求:请求头带上
    Authorization: Bearer <JWT字符串>
    
  5. 后端校验
    1. 拆分三段,Base64 解码 Header、Payload
    2. 校验 exp 过期时间
    3. 同一份秘钥重新计算签名,和客户端传来的签名对比
    4. 一致 = 合法,解析用户信息执行业务;不一致 = 篡改/非法,拒绝请求

四、Python 简单实战(生成 & 解析 JWT)

使用库:pyjwt
安装:

pip install pyjwt

1. 生成 JWT

import jwt
from datetime import datetime, timedelta

SECRET_KEY = "你的安全秘钥"  # 项目配置里保存,不要硬编码暴露
ALGORITHM = "HS256"

def create_token(user_id: int, username: str):
    payload = {
        "sub": user_id,
        "name": username,
        "iat": datetime.utcnow(),
        # 过期时间:当前时间 + 2小时
        "exp": datetime.utcnow() + timedelta(hours=2)
    }
    token = jwt.encode(payload, SECRET_KEY, algorithm=ALGORITHM)
    return token

token = create_token(1001, "zhangsan")
print(token)

2. 解析 & 校验 JWT

def verify_token(token: str):
    try:
        # 自动校验签名 + 过期时间
        payload = jwt.decode(token, SECRET_KEY, algorithms=[ALGORITHM])
        return payload  # 解析出用户信息
    except jwt.ExpiredSignatureError:
        return "Token 已过期"
    except jwt.InvalidTokenError:
        return "Token 非法/被篡改"

res = verify_token(token)
print(res)

五、JWT vs 传统 Session

对比 Session JWT
服务端状态 有状态(存会话) 无状态(不存)
集群部署 需共享 Session(Redis) 天然支持分布式
数据存放 服务端 Token 内部
安全性 相对高 载荷可解码,不能存敏感数据
跨域 受限 友好

六、优点 & 缺点

优点

  1. 无状态,易扩展、适配微服务/集群
  2. 跨端、跨域友好,移动端、小程序通用
  3. 减少服务端存储压力
  4. 自带信息,减少数据库/Redis 查询

缺点

  1. Token 一旦签发,无法主动作废(过期前一直有效)
    解决:短期过期 + 维护黑名单(Redis)
  2. Payload 只是编码,非加密,禁止存敏感数据
  3. Token 较长,每次请求都携带,略微增加请求体积
  4. 续签逻辑比 Session 复杂

七、生产环境最佳实践

  1. 设置合理短过期时间(1~2小时),搭配刷新令牌 Refresh Token
  2. 秘钥复杂度高,环境变量/配置中心管理,绝不硬编码
  3. HTTPS 必须开启,防止 Token 被劫持
  4. 敏感接口增加 Redis 黑名单,实现强制登出
  5. 尽量使用 HS256 / RS256 非对称加密(更高安全)
  6. 前端优先放 HttpOnly Cookie,防止 XSS 窃取

八、常见面试一句话总结

JWT 是无状态身份令牌,由头部、载荷、签名三部分组成;载荷可解码不可存敏感信息,签名保证防篡改;依靠过期时间控制有效期,适合分布式系统做登录认证。



Starlette 路径转换器

Starlette 路由支持路径转换器,用于对 URL 路径参数做类型解析、格式校验、正则匹配,用法和 FastAPI、Flask 思路相近,语法为 {param:converter}

一、基础语法格式

路由参数声明格式:

{参数名:转换器}

示例:/{id:int} 表示把路径片段转为整型,非数字会直接 404。


二、内置标准转换器

Starlette 自带 6 种内置转换器,开箱即用:

1. str 字符串(默认)

  • 匹配:/ 以外的所有字符
  • 简写:{name} 等价 {name:str}
from starlette.applications import Starlette
from starlette.routing import Route
from starlette.responses import PlainTextResponse

async def hello(request):
    name = request.path_params["name"]
    return PlainTextResponse(f"Hello {name}")

routes = [
    Route("/{name:str}", hello)
]
app = Starlette(routes=routes)

2. int 整数

  • 匹配:十进制整数,负数也支持
  • 非数字 → 路由不匹配,返回 404
Route("/user/{uid:int}", user_handler)

3. float 浮点数

  • 匹配:小数、整数
Route("/price/{num:float}", price_handler)

4. uuid UUID

  • 严格匹配标准 UUID 格式(xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx
  • 常用于订单ID、设备ID、用户唯一标识
import uuid
async def get_order(request):
    order_id: uuid.UUID = request.path_params["oid"]
    return PlainTextResponse(f"Order: {order_id}")

Route("/order/{oid:uuid}", get_order)

5. path 完整路径

  • 匹配:包含 / 的完整路径片段(会贪婪匹配剩余所有路径)
  • 适用:文件路径、多级路由、静态资源转发
# /files/a/b/c.txt → file_path = "a/b/c.txt"
Route("/files/{file_path:path}", file_handler)

6. enum 枚举匹配(固定可选值)

限制参数只能是指定列表内的值,语法:
{param:enum(选项1,选项2,选项3)}

# 只允许 get / post
Route("/api/{method:enum(get,post)}", api_handler)

三、完整可运行示例

from starlette.applications import Starlette
from starlette.routing import Route
from starlette.responses import PlainTextResponse

async def demo_int(request):
    uid = request.path_params["uid"]
    return PlainTextResponse(f"整型ID: {uid}, 类型:{type(uid)}")

async def demo_float(request):
    val = request.path_params["val"]
    return PlainTextResponse(f"浮点数: {val}")

async def demo_path(request):
    path = request.path_params["p"]
    return PlainTextResponse(f"完整路径: {path}")

async def demo_enum(request):
    action = request.path_params["act"]
    return PlainTextResponse(f"操作: {action}")

routes = [
    Route("/user/{uid:int}", demo_int),
    Route("/num/{val:float}", demo_float),
    Route("/res/{p:path}", demo_path),
    Route("/do/{act:enum(add,delete,query)}", demo_enum),
]

app = Starlette(routes=routes)

测试请求:

  • /user/123 → 匹配成功,uid=123(int)
  • /user/abc → 404
  • /res/a/b/cp = "a/b/c"
  • /do/add → 匹配,act="add"

四、自定义路径转换器(高阶)

内置不满足时,可以自定义正则转换器,步骤:

  1. 继承 starlette.routing.Converter
  2. 重写 regex 正则、convert 转换方法
  3. 注册转换器
  4. 路由中使用

示例:自定义手机号转换器

import re
from starlette.applications import Starlette
from starlette.routing import Route, Converter
from starlette.responses import PlainTextResponse

# 1. 自定义转换器
class PhoneConverter(Converter):
    # 正则:中国大陆手机号
    regex = r"1[3-9]\d{9}"

    def convert(self, value: str):
        # 可做类型转换/清洗
        return value

# 2. 注册转换器
Route.converters["phone"] = PhoneConverter

# 3. 使用
async def get_user(request):
    phone = request.path_params["tel"]
    return PlainTextResponse(f"手机号: {phone}")

routes = [
    Route("/u/{tel:phone}", get_user)
]

app = Starlette(routes=routes)

测试:

  • /u/13800138000 → 匹配
  • /u/123456 → 正则不匹配,404

五、关键要点 & 常见坑

  1. 优先级
    路由从上到下匹配,精准路由放前面,path 转换器放最后。
  2. path 贪婪匹配
    {x:path} 会吃掉后面所有 /,不要和普通路由混用在同一路由前缀。
  3. 自动类型转换
    int/float/uuid 会自动转为对应类型,无需手动强转。
  4. enum 严格匹配
    大小写敏感,只能传预设值。
  5. 转换器只做路由匹配+参数转换,不做业务校验,复杂校验建议在视图函数内补充。

六、速查表

转换器 作用 示例
str 普通字符串(默认) /{name}
int 整数 /{id:int}
float 浮点数 /{score:float}
uuid UUID /{token:uuid}
path / 完整路径 /{file:path}
enum(a,b) 枚举限定值 /{op:enum(yes,no)}


exclude 与 include_in_schema 的区别

Pydantic 的 Field(exclude=...) 和 FastAPI Query(include_in_schema=False) 功能场景相近,但作用对象、生效时机、用途完全不同,下面分清楚用法、区别、实战示例。

一、先分清两个核心概念

  1. Query(include_in_schema=False)
    作用:隐藏接口文档(OpenAPI/Swagger)
    主体:FastAPI 路由的查询参数/路径参数
    范围:只影响接口文档展示,不影响请求、序列化、模型字段

  2. Field(exclude=...)
    作用:序列化/导出时剔除模型字段
    主体:Pydantic 数据模型字段
    范围:控制 model_dump() / dict() / JSON 输出时要不要丢掉该字段


二、1. Pydantic Field 之 exclude 详解

基础语法

exclude 接收 bool,也可结合序列化上下文精细控制。

from pydantic import BaseModel, Field
from typing import Annotated

class User(BaseModel):
    # 普通字段,正常序列化
    username: str
    # 序列化时直接排除该字段(接口返回、转字典都看不到)
    password: Annotated[str, Field(exclude=True)]
    # 可选字段,同样排除
    token: Annotated[str | None, Field(exclude=True)] = None
测试
u = User(username="zhangsan", password="123456", token="abc123")
print(u.model_dump())
# 输出:{'username': 'zhangsan'}
# password、token 被彻底剔除
关键特性
  • exclude=True无论如何,序列化时都不出现
  • 只影响输出:请求传入该字段依然可以正常解析、存入模型,只是返回时隐藏
  • 典型场景:密码、密钥、内部 ID、敏感字段

进阶:exclude 细粒度控制(Pydantic 常用)

还可以按模式区分(include/exclude 字典),区分创建、响应等场景:

class User(BaseModel):
    username: str
    # 仅在接口响应(return)时排除,解析请求时保留
    secret: Annotated[str, Field(exclude={"response"})]

三、2. 结合 FastAPI 实战(最常用组合)

场景1:模型敏感字段 → Field(exclude=True)

后端接收密码,但返回前端永远隐藏

from fastapi import FastAPI
from pydantic import BaseModel, Field
from typing import Annotated

app = FastAPI()

class UserCreate(BaseModel):
    username: str
    # 密码:接收参数,但序列化返回时排除
    password: Annotated[str, Field(exclude=True)]

@app.post("/user")
async def create_user(user: UserCreate):
    print(user.password)  # 后端可以正常拿到密码
    return user           # 前端响应里看不到 password

场景2:路由查询参数隐藏文档 + 模型字段排除(对比演示)
from typing import Annotated
from fastapi import FastAPI, Query
from pydantic import BaseModel, Field

app = FastAPI()

# ========== 1. 模型字段:Field(exclude) 控制返回结果 ==========
class Item(BaseModel):
    name: str
    # 序列化输出时剔除
    inner_code: Annotated[str, Field(exclude=True)]

@app.post("/item")
async def create_item(item: Item):
    return item  # 响应无 inner_code

# ========== 2. 路由参数:Query(include_in_schema=False) 控制文档 ==========
@app.get("/query-demo")
async def query_demo(
    hidden_param: Annotated[
        str | None,
        Query(include_in_schema=False)  # 仅隐藏 docs 文档
    ] = None
):
    return {"data": hidden_param}

四、核心区别对照表(重点记忆)

配置项 所属模块 作用对象 核心效果 典型用途
Query(include_in_schema=False) FastAPI 路由参数 URL 查询/路径参数 不在 Swagger 接口文档显示,请求、返回完全正常 内部调试参数、兼容旧参数、不想对外暴露接口字段
Field(exclude=True) Pydantic 模型字段 数据模型属性 序列化(dict/JSON)时直接剔除字段,后端可正常读取 密码、密钥、敏感数据、内部字段

五、易混场景 & 常见误区

误区1:两者都叫“隐藏”,可以互换?

❌ 不行:

  • 隐藏接口文档 → 用 include_in_schema=False
  • 不让字段返回给前端 → 用 Field(exclude=True)

误区2:Field(exclude=True) 会拒绝接收参数?

❌ 不会:
请求依然可以传该字段,后端能读到,只是响应不返回

误区3:能不能同时使用?

✅ 可以组合使用(生产很常见):

  1. 路由参数隐藏文档
  2. 模型字段返回时剔除
class Params(BaseModel):
    inner_key: Annotated[str, Field(exclude=True)]

@app.get("/mix")
async def mix_demo(
    inner_key: Annotated[
        str | None,
        Query(include_in_schema=False)
    ] = None
):
    p = Params(inner_key=inner_key or "")
    return p

六、补充:Pydantic v2 额外小知识点

  1. Field(exclude) 优先级高于 model_dump(exclude=...)
  2. 如果只是单次序列化排除,不用改 Field,调用时临时排除:
    user.model_dump(exclude={"password"})
    
  3. Field(include=True) 是默认行为,一般不用写。

一句话总结

  • include_in_schema=False:藏在 API 文档 里,接口照常可用;
  • Field(exclude=True):藏在 JSON 返回结果 里,后端照常取值。


如果请求体的最外层就是一个 JSON 数组,可以直接在函数参数中声明列表类型

from fastapi import FastAPI
from pydantic import BaseModel, HttpUrl

app = FastAPI()


class Image(BaseModel):
    url: HttpUrl
    name: str


# 请求体直接是一个 Image 列表
@app.post("/images/multiple/")
async def create_multiple_images(images: list[Image]):
    return images

一、核心结论

FastAPI 支持根层级为 JSON 数组的请求体,你只需要在接口参数里直接声明列表类型list[模型类]),框架会自动解析最外层的 JSON 数组。

常规请求体大多是JSON 对象 {},而这里接收的是 JSON 数组 []


二、逐段拆解代码

1. 模型定义

from fastapi import FastAPI
from pydantic import BaseModel, HttpUrl

app = FastAPI()

class Image(BaseModel):
    url: HttpUrl  # Pydantic 自动校验合法 URL
    name: str

Image 是普通 Pydantic 模型,用来约束单张图片的字段结构。

2. 接口声明(关键)

# 接收:整个请求体 = Image 对象组成的 JSON 数组
@app.post("/images/multiple/")
async def create_multiple_images(images: list[Image]):
    return images
  • 参数 images: list[Image]
    告诉 FastAPI:整个 POST 请求体,不是单个对象,而是一个数组,数组里每一项都要符合 Image 模型规则。
  • 不再需要额外包一层外层对象,这是 FastAPI 原生支持的特性。

三、两种请求体对比(重点区分)

场景1:常规写法(外层是 JSON 对象)

如果参数写 images: Image,要求请求体是单个 JSON 对象

{
  "url": "https://test.com/1.jpg",
  "name": "pic1"
}

场景2:当前代码(外层是 JSON 数组)

参数写 images: list[Image],要求请求体最外层直接是 [] 数组,也是你代码的用法:

[
  {
    "url": "https://test.com/1.jpg",
    "name": "pic1"
  },
  {
    "url": "https://test.com/2.jpg",
    "name": "pic2"
  }
]

✅ 这就是「请求体最外层是 JSON 数组,直接声明列表类型」的含义。


四、运行测试演示

  1. 启动服务
uvicorn main:app --reload
  1. 访问文档 http://127.0.0.1:8000/docs
    你会看到接口请求示例直接是数组格式,可在线调试。

  2. 发起 POST 请求(curl 示例)

curl -X POST "http://127.0.0.1:8000/images/multiple/" \
-H "Content-Type: application/json" \
-d '[
  {"url": "https://a.com/1.png", "name": "img1"},
  {"url": "https://a.com/2.png", "name": "img2"}
]'

响应原样返回数组,解析成功。


五、补充规则 & 常见疑问

1. 为什么能直接用列表?

FastAPI + Pydantic 深度整合:

  • 检测到参数类型是列表,就直接把整个请求体当作数组解析;
  • 列表内的每一个元素,自动用 Image 模型做字段校验、类型转换(比如 HttpUrl 校验 URL 合法性)。

2. 和「包一层外层对象」的区别(业务选型)

方式A:直接接收数组(你的代码)

请求体:[{}, {}]
适用:批量新增、纯数组数据场景。

方式B:外层包一个对象(传统写法)

定义嵌套模型:

class ImageList(BaseModel):
    images: list[Image]

@app.post("/images/")
async def create_images(data: ImageList):
    return data.images

请求体必须是:

{
  "images": [{}, {}]
}

适用:接口需要扩展额外字段(如 batch_idtimestamp)的场景。

3. 限制说明

  • 只有请求体可以这样声明根数组;
  • 查询参数、路径参数不能直接用根数组;
  • 数组内元素会严格遵循 Pydantic 校验,格式错误直接返回 422 校验失败。

六、一句话总结

当 POST 请求的 JSON 最外层是 [] 数组时,不用额外封装外层模型,直接将接口参数标注为 list[你的Pydantic模型],FastAPI 就会自动解析整个请求体为对象列表。

根数组,对象内数组,外层对象,外层数组

一、这些名字是谁起的?

  1. 不是某个人专门发明的术语
    后端/接口开发圈的口语俗称,为了口头沟通、排错方便,大家约定俗成叫出来的。
  2. 底层依据:JSON 语法结构
    JSON 文档整体只有一个根元素,所以才有:
    • 外层 / 根 = 整个 JSON 的最顶层
    • 内部 = 根元素里面嵌套的内容

简单说:为了描述“层级位置”而生的通俗叫法,不是官方文档术语,但行业人人都这么用。


二、先统一:一套固定叫法(以后就按这套记)

只记 4 个核心词,两两组合:

  1. 外层 / 根 = 整个请求体 JSON 的最顶部、最外面那一层
  2. 内层 / 内部 = 被包裹在里面的内容
  3. 对象 = { ... }
  4. 数组 = [ ... ]

组合后就四个常用说法:

  • 外层对象(根对象):最顶层是 {}
  • 外层数组(根数组):最顶层是 []
  • 对象内数组:{} 里面放了 []
  • 数组内对象:[] 里面放了 {}

三、逐种结构 + 图示 + 通俗解释(一看就懂)

下面全部用完整请求体 JSON 展示,对应 FastAPI 用法。

1. 外层对象(根对象)【最常用、90% 接口】

结构

整个 JSON 最外面一圈是大括号 {}

{
  "url": "https://a.jpg",
  "name": "图片1"
}
  • 外层/根:{}外层对象
  • 里面全是普通字段,没有数组
对应 FastAPI
class Image(BaseModel):
    url: HttpUrl
    name: str

@app.post("/img")
async def f(img: Image):  # 参数 = 单个模型
    ...

2. 对象内数组(外层对象 + 内部嵌套数组)【企业批量接口首选】

结构

最外层依然是 {}(外层对象),
在大括号里面,放了一个 [] 数组

{
  "batch_id": "B001",
  "img_list": [   // ✅ 这就是:对象内数组
    {"url": "a.jpg", "name": "图1"},
    {"url": "b.jpg", "name": "图2"}
  ]
}

拆解:

  • 最外层:{} → 外层对象
  • img_list 对应的值:[]对象内数组
对应 FastAPI
class Image(BaseModel):
    url: HttpUrl
    name: str

class Batch(BaseModel):
    batch_id: str
    img_list: list[Image]  # 模型里声明列表

@app.post("/batch")
async def f(data: Batch):
    ...

👉 这就是你平时写得最多的批量接口,绝大多数人误以为“请求体是数组”,其实只是对象里面包了数组


3. 外层数组(根数组)【小众、特殊用法】

结构

整个 JSON 最外面一圈直接就是中括号 [],没有外层大括号。

[  // ✅ 最顶层 = 数组 → 外层数组 / 根数组
  {"url": "a.jpg", "name": "图1"},
  {"url": "b.jpg", "name": "图2"}
]
  • 外层/根:[]外层数组(根数组)
  • 数组每一项又是 {} → 数组内对象
对应 FastAPI
@app.post("/multi")
async def f(images: list[Image]):  # 参数直接声明为 list
    ...

👉 就是你最开始示例的代码,业务里很少用


四、一张极简对照表(收藏,永久不混淆)

口头叫法 整体最外层 结构样子 使用频率
外层对象(根对象) {} { 字段: 值 } ⭐⭐⭐⭐⭐ 标配
对象内数组 {} { 数组字段: [ {}, {} ] } ⭐⭐⭐⭐⭐ 批量接口主流
外层数组(根数组) [] [ {}, {} ] ⭐ 极少用

五、为什么容易搞混?(你的懵逼根源)

  1. 平时写批量接口,都是「对象内数组」
    看多了 { "xxx": [ ... ] },大脑形成惯性:

    “批量数据 = 数组”
    于是下意识以为:整个请求体就是数组

  2. 术语里都带“数组”,但位置完全不同

    • 数组长在 里面 → 对象内数组(正常批量接口)
    • 数组长在 最外面 → 外层数组(特殊接口)

六、日常沟通简化说法(以后不用绕口)

以后和同事交流,直接这么说,简洁不歧义:

  1. 普通单条请求:外面是对象
  2. 常规批量请求:对象里面带数组
  3. 特殊纯数组请求:最外层直接是数组

七、最后一句话彻底总结

  • 外层 = 整个 JSON 的最顶头
  • {} 叫对象,[] 叫数组
  • 工作中:99% 都是「外层对象」,批量也只是对象里面塞数组
  • 只有代码参数写 list[模型] 时,才是外层直接是数组
Logo

AtomGit 是由开放原子开源基金会联合 CSDN 等生态伙伴共同推出的新一代开源与人工智能协作平台。平台坚持“开放、中立、公益”的理念,把代码托管、模型共享、数据集托管、智能体开发体验和算力服务整合在一起,为开发者提供从开发、训练到部署的一站式体验。

更多推荐