Pydantic vs Go struct + json tag:入参解析与校验练习
难度:⭐⭐
考点
- Pydantic v2:字段约束(
Field)、自定义校验器(field_validator)、model_validate_json - Go
encoding/json:json tag 映射、零值语义、指针区分"没传"与"传了空值" - 核心对照:Pydantic 构造即校验;Go 只做映射、校验必须自己写
题目描述
实现「剪辑任务入参」的解析与校验。同一份 JSON 输入,分别用 Python Pydantic 与 Go 实现。
校验规则(两语言一致):
task_id必填:长度 1~64,且不能包含空格duration必填:范围 (0, 600](秒)tags可选,默认空列表title可选,缺省为 None / nil(Go 侧用指针区分"没传"与"传了空串")
函数签名
Go(solution.go)
go
// ParseClipTask 解析 JSON 并校验;任何规则不满足都返回 error
func ParseClipTask(raw []byte) (ClipTask, error)ClipTask 结构体与 json tag 已给出,直接实现 ParseClipTask 即可。
Python(solution.py)
python
# ClipTask 模型骨架已给出,需自己补全字段约束与校验器
def parse_clip_task(raw: str) -> ClipTask:
"""解析并校验 JSON 字符串;非法输入抛 pydantic.ValidationError"""示例
json
{"task_id": "t-1", "duration": 10, "tags": ["cut"], "title": "demo"}- 合法输入 → 解析成功,字段正确映射
{"duration": 10}(缺 task_id)→ 报错{"task_id": "t 1", "duration": 10}(含空格)→ 报错{"task_id": "t-1", "duration": 601}(越界)→ 报错{"task_id": "t-1", "duration": "30"}→ Pydantic 自动转 int 成功;Go 报错(对照考点 1)
提示
- Python:
Field(min_length=..., gt=..., le=...)声明约束,@field_validator加自定义规则,最后一行ClipTask.model_validate_json(raw)即完成"解析 + 构造时校验"。 - Go:
json.Unmarshal只做映射;校验要自己写——注意缺字段是零值(task_id缺省是""、duration缺省是0),所以"必填"要通过"零值不满足约束"来体现。 - 类型不匹配时 Go 的
json.Unmarshal会直接返回 error(如字符串进 int 字段),无需自己处理。
运行(验证你的实现)
bash
# Go 侧(在 code/route 下)
cd code/route && go test ./07_python/01_pydantic_validation -v
# Python 侧(在练习目录下,需 pip install "pydantic>=2")
cd 07_python/01_pydantic_validation
python3 -m unittest test_solution.py -v # 或 python3 test_solution.py -v测试全部通过 = 掌握本节考点。参考答案见页面底部(或 answer/ 目录)。
对照考点表
| # | 考点 | Pydantic | Go |
|---|---|---|---|
| 1 | 自动类型转换 | "30" → int 30 ✓ | 不转换,"30" 进 int 字段直接报错 |
| 2 | 缺必填字段 | ValidationError(missing) | 静默变零值,靠"零值不满足约束"兜底 |
| 3 | 区分"没传"与"传了空值" | None vs "" | 指针 *string:nil vs *"" |
| 4 | 自定义校验 | @field_validator | 手动 Validate() / UnmarshalJSON |
| 5 | 空值输出控制 | exclude_none=True | omitempty tag |
| 6 | 反序列化入口 | model_validate_json() | json.Unmarshal |
| 7 | 校验时机 | 构造对象时自动校验 | 默认不校验,需手写或引入 validator 库 |
参考答案(Go)
点击展开参考答案
go
//go:build ignore
package answer
import (
"encoding/json"
"errors"
"fmt"
"strings"
)
// ClipTask 与 solution.go 保持一致
type ClipTask struct {
TaskID string `json:"task_id"`
Duration int `json:"duration"`
Tags []string `json:"tags,omitempty"`
Title *string `json:"title,omitempty"`
}
// ErrInvalid 校验失败统一错误
var ErrInvalid = errors.New("invalid clip task")
// ParseClipTask 参考答案:json.Unmarshal 做映射(类型不匹配会返回 error),
// 再手动校验约束——"必填"通过"零值不满足约束"体现。
func ParseClipTask(raw []byte) (ClipTask, error) {
var t ClipTask
if err := json.Unmarshal(raw, &t); err != nil {
return ClipTask{}, err // JSON 语法错误 / 类型不匹配
}
if len(t.TaskID) < 1 || len(t.TaskID) > 64 {
return ClipTask{}, fmt.Errorf("%w: task_id 长度必须 1~64", ErrInvalid)
}
if strings.Contains(t.TaskID, " ") {
return ClipTask{}, fmt.Errorf("%w: task_id 不能包含空格", ErrInvalid)
}
if t.Duration <= 0 || t.Duration > 600 {
return ClipTask{}, fmt.Errorf("%w: duration 必须 (0,600]", ErrInvalid)
}
return t, nil
}参考答案(Python)
点击展开参考答案
py
"""参考答案:Pydantic v2 定义 + 解析函数(与 Go 侧 ParseClipTask 对照)。
与 solution.py 的 TODO 对应,先独立完成再对照。
"""
from __future__ import annotations
from typing import Optional
from pydantic import BaseModel, Field, field_validator
class ClipTask(BaseModel):
task_id: str = Field(min_length=1, max_length=64) # 必填 + 长度约束
duration: int = Field(gt=0, le=600) # 必填 + 范围约束
tags: list[str] = Field(default_factory=list) # 可选,默认 []
title: Optional[str] = None # 可选,缺省 None
@field_validator("task_id")
@classmethod
def no_space(cls, v: str) -> str:
if " " in v:
raise ValueError("task_id 不能包含空格")
return v
def parse_clip_task(raw: str) -> ClipTask:
"""一行实现:解析 + 构造时自动校验(字段约束与 validator 全部生效)。"""
return ClipTask.model_validate_json(raw)