【Fastapi学习笔记(5)】—— Union、JWT token、Starlette 的路径转换器、exclude与include_in_schema区别、根对象/根数组/对象内数组/数组内对象
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
三、易错点 & 注意事项
-
Union 是「或」关系
表示满足其中一种即可,不是同时为多个类型。 -
Union 去重
Union[int, int, str]等价于Union[int, str],重复类型会自动合并。 -
不要滥用 Union
- 过多类型(如十几种)会降低代码可读性、破坏类型提示意义;
- 优先考虑基类/抽象类、重载函数,而非无脑堆 Union。
-
和
Any的区别Any:任意类型,关闭类型检查;Union[A,B]:限定只能是 A/B,依然有类型约束,优先用 Union。
-
旧版本 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);
}
快速总结
- 作用:限定变量/参数属于多种类型之一;
- Python 写法:
- 旧版:
from typing import Union+Union[T1, T2] - 3.10+ 推荐:
T1 | T2简写;
- 旧版:
Optional[T]=T | None,专门处理可空场景;- 运行时用
isinstance()判断实际类型; - 多用于类型注解、接口参数、数据模型(如 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
自定义字段
自己加:username、role、phone 等非敏感信息
⚠️ 重点:Header 和 Payload 只是 Base64 编码,不是加密!可直接解码查看
绝对不要放:密码、密钥、银行卡等敏感数据。
3. Signature 签名(第三段,安全核心)
防止 Token 被篡改
生成规则:
签名 = HMAC-SHA256( Header.Base64 + "." + Payload.Base64, 服务端秘钥 )
- 秘钥只有后端知道
- 只要 Header / Payload 任意字符被修改,签名校验必然失败
三、完整登录 & 请求流程
- 用户登录:提交账号密码 → 后端校验
- 生成 JWT:校验通过,组装 Header + Payload,用秘钥签名,返回完整 JWT
- 前端存储:一般存在
localStorage/Cookie - 后续接口请求:请求头带上
Authorization: Bearer <JWT字符串> - 后端校验
- 拆分三段,Base64 解码 Header、Payload
- 校验
exp过期时间 - 用同一份秘钥重新计算签名,和客户端传来的签名对比
- 一致 = 合法,解析用户信息执行业务;不一致 = 篡改/非法,拒绝请求
四、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 内部 |
| 安全性 | 相对高 | 载荷可解码,不能存敏感数据 |
| 跨域 | 受限 | 友好 |
六、优点 & 缺点
优点
- 无状态,易扩展、适配微服务/集群
- 跨端、跨域友好,移动端、小程序通用
- 减少服务端存储压力
- 自带信息,减少数据库/Redis 查询
缺点
- Token 一旦签发,无法主动作废(过期前一直有效)
解决:短期过期 + 维护黑名单(Redis) - Payload 只是编码,非加密,禁止存敏感数据
- Token 较长,每次请求都携带,略微增加请求体积
- 续签逻辑比 Session 复杂
七、生产环境最佳实践
- 设置合理短过期时间(1~2小时),搭配刷新令牌
Refresh Token - 秘钥复杂度高,环境变量/配置中心管理,绝不硬编码
- HTTPS 必须开启,防止 Token 被劫持
- 敏感接口增加 Redis 黑名单,实现强制登出
- 尽量使用
HS256/RS256非对称加密(更高安全) - 前端优先放
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/c→p = "a/b/c"/do/add→ 匹配,act="add"
四、自定义路径转换器(高阶)
内置不满足时,可以自定义正则转换器,步骤:
- 继承
starlette.routing.Converter - 重写
regex正则、convert转换方法 - 注册转换器
- 路由中使用
示例:自定义手机号转换器
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
五、关键要点 & 常见坑
- 优先级
路由从上到下匹配,精准路由放前面,path转换器放最后。 path贪婪匹配{x:path}会吃掉后面所有/,不要和普通路由混用在同一路由前缀。- 自动类型转换
int/float/uuid会自动转为对应类型,无需手动强转。 - enum 严格匹配
大小写敏感,只能传预设值。 - 转换器只做路由匹配+参数转换,不做业务校验,复杂校验建议在视图函数内补充。
六、速查表
| 转换器 | 作用 | 示例 |
|---|---|---|
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) 功能场景相近,但作用对象、生效时机、用途完全不同,下面分清楚用法、区别、实战示例。
一、先分清两个核心概念
-
Query(include_in_schema=False)
作用:隐藏接口文档(OpenAPI/Swagger)
主体:FastAPI 路由的查询参数/路径参数
范围:只影响接口文档展示,不影响请求、序列化、模型字段 -
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:能不能同时使用?
✅ 可以组合使用(生产很常见):
- 路由参数隐藏文档
- 模型字段返回时剔除
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 额外小知识点
Field(exclude)优先级高于model_dump(exclude=...)- 如果只是单次序列化排除,不用改 Field,调用时临时排除:
user.model_dump(exclude={"password"}) 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 数组,直接声明列表类型」的含义。
四、运行测试演示
- 启动服务
uvicorn main:app --reload
-
访问文档
http://127.0.0.1:8000/docs
你会看到接口请求示例直接是数组格式,可在线调试。 -
发起 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_id、timestamp)的场景。
3. 限制说明
- 只有请求体可以这样声明根数组;
- 查询参数、路径参数不能直接用根数组;
- 数组内元素会严格遵循 Pydantic 校验,格式错误直接返回 422 校验失败。
六、一句话总结
当 POST 请求的 JSON 最外层是 [] 数组时,不用额外封装外层模型,直接将接口参数标注为 list[你的Pydantic模型],FastAPI 就会自动解析整个请求体为对象列表。
根数组,对象内数组,外层对象,外层数组
一、这些名字是谁起的?
- 不是某个人专门发明的术语
是后端/接口开发圈的口语俗称,为了口头沟通、排错方便,大家约定俗成叫出来的。 - 底层依据:JSON 语法结构
JSON 文档整体只有一个根元素,所以才有:- 外层 / 根 = 整个 JSON 的最顶层
- 内部 = 根元素里面嵌套的内容
简单说:为了描述“层级位置”而生的通俗叫法,不是官方文档术语,但行业人人都这么用。
二、先统一:一套固定叫法(以后就按这套记)
只记 4 个核心词,两两组合:
- 外层 / 根 = 整个请求体 JSON 的最顶部、最外面那一层
- 内层 / 内部 = 被包裹在里面的内容
- 对象 =
{ ... } - 数组 =
[ ... ]
组合后就四个常用说法:
- 外层对象(根对象):最顶层是
{} - 外层数组(根数组):最顶层是
[] - 对象内数组:
{}里面放了[] - 数组内对象:
[]里面放了{}
三、逐种结构 + 图示 + 通俗解释(一看就懂)
下面全部用完整请求体 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
...
👉 就是你最开始示例的代码,业务里很少用。
四、一张极简对照表(收藏,永久不混淆)
| 口头叫法 | 整体最外层 | 结构样子 | 使用频率 |
|---|---|---|---|
| 外层对象(根对象) | {} |
{ 字段: 值 } |
⭐⭐⭐⭐⭐ 标配 |
| 对象内数组 | {} |
{ 数组字段: [ {}, {} ] } |
⭐⭐⭐⭐⭐ 批量接口主流 |
| 外层数组(根数组) | [] |
[ {}, {} ] |
⭐ 极少用 |
五、为什么容易搞混?(你的懵逼根源)
-
平时写批量接口,都是「对象内数组」
看多了{ "xxx": [ ... ] },大脑形成惯性:“批量数据 = 数组”
于是下意识以为:整个请求体就是数组。 -
术语里都带“数组”,但位置完全不同
- 数组长在 里面 → 对象内数组(正常批量接口)
- 数组长在 最外面 → 外层数组(特殊接口)
六、日常沟通简化说法(以后不用绕口)
以后和同事交流,直接这么说,简洁不歧义:
- 普通单条请求:外面是对象
- 常规批量请求:对象里面带数组
- 特殊纯数组请求:最外层直接是数组
七、最后一句话彻底总结
- 外层 = 整个 JSON 的最顶头
{}叫对象,[]叫数组- 工作中:99% 都是「外层对象」,批量也只是对象里面塞数组;
- 只有代码参数写
list[模型]时,才是外层直接是数组。
AtomGit 是由开放原子开源基金会联合 CSDN 等生态伙伴共同推出的新一代开源与人工智能协作平台。平台坚持“开放、中立、公益”的理念,把代码托管、模型共享、数据集托管、智能体开发体验和算力服务整合在一起,为开发者提供从开发、训练到部署的一站式体验。
更多推荐



所有评论(0)