如何用Pydantic建模首元素固定、后续元素可重复的API请求列表?
问题翻译
我需要为一个接收JSON对象列表的API创建Pydantic模型,请求负载示例如下:
[{"header": "pets"}, {"meows": true}, {"barks": false}, {"meows": true}]
对应的类型逻辑应该是[Header, Pet, ...]——也就是第一个元素必须是Header类型,后面可以重复多个Pet类型。我尝试过几种方式但都不太满意:
- 使用Tuple的话,只能定义
Tuple[Header, Pet],没法支持多个重复的Pet; - 目前用
RootModel[list[Union[Header, Pet]]]实现,但需要写大量繁琐的手动验证逻辑。
如果能让Union自动排除Header(只允许第一个元素是Header),或者采用左到右的匹配逻辑就方便多了,请问有没有更优雅的实现方法?
解决方案:自定义RootModel结合结构验证
针对这种"首元素固定类型,后续元素为可重复类型"的结构,你可以通过自定义RootModel并添加验证器实现,无需手动编写大量重复逻辑,同时保留Pydantic的自动解析和错误提示能力。
1. 定义基础模型
先创建Header和Pet两个核心模型:
from pydantic import BaseModel, RootModel, ValidationError, field_validator from typing import List, Union class Header(BaseModel): header: str class Pet(BaseModel): meows: bool | None = None barks: bool | None = None # 确保Pet至少包含一个有效字段 @field_validator('meows', 'barks', mode='before') def check_at_least_one_field(cls, v, values): if all(val is None for val in values.data.values()): raise ValueError('Pet must have at least one field (meows or barks)') return v
2. 自定义Payload模型并添加结构验证
继承RootModel,通过验证器强制首元素为Header、后续元素为Pet:
class Payload(RootModel[List[Union[Header, Pet]]]): @field_validator('root', mode='before') def validate_payload_structure(cls, value): if not isinstance(value, list) or len(value) == 0: raise ValueError('Payload must be a non-empty list') # 验证并解析第一个元素为Header try: value[0] = Header(**value[0]) except ValidationError: raise ValueError('First element must be a valid Header object') # 验证并解析剩余元素为Pet for idx, item in enumerate(value[1:], start=1): try: value[idx] = Pet(**item) except ValidationError: raise ValueError(f'Element at index {idx} is not a valid Pet object') return value
3. 使用示例
# 有效负载 valid_data = [{"header": "pets"}, {"meows": True}, {"barks": False}, {"meows": True}] payload = Payload(root=valid_data) print(payload.root[0]) # Header(header='pets') print(payload.root[1]) # Pet(meows=True, barks=None) # 无效负载(首元素不是Header) invalid_data = [{"meows": True}, {"header": "pets"}] try: Payload(root=invalid_data) except ValidationError as e: print(e) # 抛出标准验证错误,提示首元素必须是Header
更简洁的实现思路
如果不想手动捕获ValidationError,可以直接在验证器中完成解析,Pydantic会自动抛出标准化错误:
class Payload(RootModel[List[Union[Header, Pet]]]): @field_validator('root', mode='before') def validate_and_parse(cls, value): if not value: raise ValueError('Payload cannot be empty') # 强制首元素为Header,后续为Pet return [Header(**value[0])] + [Pet(**item) for item in value[1:]]
为什么不直接用Union?
list[Union[Header, Pet]]的匹配逻辑是尝试所有可能的类型,不会限制Header只能出现在首位置。通过自定义验证器,我们可以强制业务规则,同时保留Pydantic的自动解析和错误提示能力。
内容的提问来源于stack exchange,提问作者Bufke
相关产品推荐
相关产品推荐

