Skip to content
返回博客
教程

JSON 转 Python:dataclass、Pydantic 与 TypedDict 完全指南(2026)

正确地把 JSON 转成 Python 类:dataclass、Pydantic v2、TypedDict 三种模式,Optional 类型推断、camelCase 别名与常见陷阱。免费在线,数据不离开浏览器。

12 分钟

JSON 转 Python:dataclass、Pydantic 与 TypedDict 完全指南(2026)

把 JSON 粘进 JSON 转 Python 类转换器,复制它生成的类即可。这就是 JSON 转 Python 的快捷路径:无需安装、无需上传,一切都在浏览器里完成。临时建个模型,或快速看一眼陌生 API 的响应,几秒钟就搞定。

更难的问题是转换器替你解决不了的那个。它能推断出正确的类型,却做不了最关键的决定:三种输出里你到底要哪一个。dataclass、Pydantic v2 的 BaseModelTypedDict 可以描述完全相同的 JSON,但它们在类型安全强度、运行时行为、以及数据如何载入这几方面各不相同。选错了,要么白白背上 Pydantic 的依赖,要么以为 dataclass 会校验输入,其实它悄悄地什么都没做。

本文覆盖这三者的决策矩阵、转换器如何推断类型、各模式下 camelCase 的处理、如何用生成的类解析 JSON(包括那个坑住很多人的嵌套陷阱)、Pydantic v2 与 v1 的差异,以及在信任输出之前值得了解的边缘情况。

如何把 JSON 转成 Python

把 JSON 转成 Python 只需三步:

  1. 粘贴 JSON。 把对象、数组或原始 API 响应丢进输入框。转换即时完成,且完全在客户端运行。
  2. 选择 dataclass、Pydantic 还是 TypedDict。 用 Output 开关切换输出风格,并把默认的 Root 改成有意义的名字,比如 UserApiResponse
  3. 复制或下载。 一键取走生成的 Python 代码,把 models.py 直接放进项目。

这就是完整的操作路径。如果输入被压缩过,或你不确定它是否合法,先用 JSON 格式化过一遍,让转换器读到干净、规整的 JSON。本文余下部分解释输出内容,帮你处理工具无法自行推断的那些情况。

JSON 类型如何映射到 Python

每个 JSON 值都有对应的 Python 类型,而且这套映射很直白:

JSON 值Python 类型
"text"str
42int(任意精度,无溢出)
3.14, 2e3float
true / falsebool
nullOptional[T] / Optional[Any]
[1, 2, 3]List[T]
{ ... }具名 class

拿一个典型的 REST payload 来说:

{ "id": 101, "name": "Ada Lovelace", "email": "ada@example.com", "active": true, "roles": ["admin", "user"] }

在 dataclass 模式下,转换器生成一个开箱即用的类:

from dataclasses import dataclass
from typing import List


@dataclass
class Root:
    id: int
    name: str
    email: str
    active: bool
    roles: List[str]

这里有个 Python 特有的细节。它的 int 是任意精度的,所以 Discord 雪花 ID 或任何超过 2^53 的 ID 照样是普通的 int,毫无精度损失。同样的值在 JavaScript 里会丢精度,在 Rust 里则逼你在 i64u64f64 之间做选择。只有带小数点或指数写法的数字才会被推断为 float。一个字段如果始终只有 null,光凭样本无法定型,于是回退到 Optional[Any];粘一个填了真实值的代表性 payload,就能得到比 Any 更精确的类型。

dataclass、Pydantic v2 与 TypedDict——该用哪个?

这正是转换器丢回给你的选择,也是本文要回答的主要问题。一句话版本:想要零依赖的内部数据持有者,用 dataclass;要解析或校验不可信输入,用 Pydantic v2;只想给手头已有的 dict 加上静态类型提示,用 TypedDict

维度dataclassPydantic v2TypedDict
依赖标准库pip install pydantic标准库(typing
运行时校验有——校验并强制转换类型无——仅类型提示
运行时开销轻(真实对象)有(校验成本)零(本身就是 dict
嵌套自动解析无,需手动有,model_validate 会递归无,本身就是 dict
camelCase 别名保留原始 keyField(alias=...)functional 语法
最适合内部数据结构API 响应、webhook、配置给遗留 dict 代码加类型

对于像 {"id": 101, "name": "Ada Lovelace", "active": true} 这样的小结构,三种目标看起来几乎一模一样。JSON 转 Python dataclass 给你一个标准库对象:

from dataclasses import dataclass


@dataclass
class Root:
    id: int
    name: str
    active: bool

JSON 转 Pydantic 的版本换成了 BaseModel,看着相似,却在运行时体现出价值:

from pydantic import BaseModel


class Root(BaseModel):
    id: int
    name: str
    active: bool

而由 JSON 生成的 Python TypedDict 描述的是一个普通 dict 的形状,不额外分配任何新对象:

from typing import TypedDict


class Root(TypedDict):
    id: int
    name: str
    active: bool

同样的字段,三份截然不同的契约。从同一个样本生成全部三种的价值在于:你可以在工具里来回切换,按任务匹配模式:解析你控制不了的数据用 Pydantic,内部数据用 dataclass,只想让 mypy 看懂一个你本来就在传来传去的 dict 就用 TypedDict。

转换器如何推断类

三条规则几乎覆盖了你喂进去的一切:每种对象形状一个类、数组按 key 逐一合并、以及对缺失值的谨慎处理。

结构推断:每个对象一个具名类

每一种不同的对象形状都会成为自己的具名类。嵌套对象不会被内联,而是被提升为独立的、被引用的定义:

{ "repo": "pydantic", "owner": { "login": "samuelcolvin", "id": 100 } }
from dataclasses import dataclass


@dataclass
class Owner:
    login: str
    id: int


@dataclass
class Root:
    repo: str
    owner: Owner

嵌套的 owner 变成了自己的 Owner 类,由字段引用。注意顺序:子类在使用它的父类之前输出,所以整个模块无需 from __future__ import annotations 就能运行。相同的形状还会被去重,因此结构相同的两个字段共用一个类,而不是生成副本。

数组合并与 Optional 字段

当你传入一个对象数组时,转换器会按 key 逐一把它们合并成单一的元素类型。在部分元素里存在、在其他元素里缺失的 key 会变成 Optional

{ "users": [{ "id": 1, "nick": "x" }, { "id": 2 }] }
from typing import List, Optional, TypedDict


class User(TypedDict):
    id: int
    nick: Optional[str]


class Root(TypedDict):
    users: List[User]

id 出现在每个元素里,所以保持必填。nick 在第二个用户里缺失,于是变成 Optional[str]。这恰恰说明为什么单个对象是个很弱的样本:只有当工具真的在某处看到 key 缺失时,才会把字段标为 Optional。喂一个有代表性的数组,推断才能反映真实的形状。

Optional、None 与空数组

有几种值本身不带类型信息,转换器对此很诚实。在部分数组元素里缺失的 key 会变成 Optional[T]。始终只有 null 的字段会变成 Optional[Any],因为单凭 JSON 的 null 无法告诉你目标类型是什么。空数组,或元素类型混杂的数组,会回退到 List[Any]。这些都不是 bug,而是工具拒绝瞎猜。一旦拿到能体现具体类型的样本,就把每个 Any 换成确切的类型;另外别忘了,Python 的任意精度 int 意味着一个大 ID 永远不会像在 Rust 或 JavaScript 里那样,逼你换用更宽的数值类型。

各模式下如何处理 camelCase key

JSON 的 key 常是 camelCase,而惯用的 Python 字段是 snake_case。这里没有唯一正确的答案,每种模式的处理方式都不同,复制输出前值得弄清楚这些差异。

dataclass。 dataclass 没有内建的别名机制。为了让 Root(**data) 正常工作,只要原始 key 是合法的 Python 标识符,转换器就把它原样保留作字段名,所以 publicRepos 仍是 publicRepos。强行改成 snake_case 会让 **data 抛出 TypeError

Pydantic v2。 这里你能得到惯用的结果。字段被改名为 snake_case,并用 Field(alias=...) 映射回精确的 JSON key,再加上 model_config = ConfigDict(populate_by_name=True),这样你既能用字段名也能用别名来构造模型:

from pydantic import BaseModel, ConfigDict, Field


class Root(BaseModel):
    model_config = ConfigDict(populate_by_name=True)

    login: str
    public_repos: int = Field(alias="publicRepos")
    created_at: str = Field(alias="createdAt")

正是这一点,让 Root.model_validate(api_json) 能够 round-trip 真实的 camelCase payload,同时你的代码读起来又很自然。

TypedDict。 普通的标识符 key 直接使用。一旦出现非标识符的 key,转换器就切换到 functional 的 TypedDict('User', {...}) 语法来保留精确的 key,这一点下一节会讲到。

Python 关键字与非标识符 key

JSON 的 key 只是字符串,所以没什么能拦住一个 API 发来 classfromfirst-name。它们要是直接当作 Python 属性名,都会是语法错误,因此转换器会对它们做消毒处理。

身为 Python 关键字的 key 会被加上尾部下划线:class 变成 class_from 变成 from_。在 Pydantic 模式下,它还会保留一个 Field(alias=...),让模型仍能读到原始的传输 key:

from pydantic import BaseModel, ConfigDict, Field


class Root(BaseModel):
    model_config = ConfigDict(populate_by_name=True)

    class_: str = Field(alias="class")
    from_: str = Field(alias="from")

带连字符、空格或以数字开头的 key,在 dataclass 和 Pydantic 模式下会被消毒成合法标识符。TypedDict 是唯一能保留字面 key 的模式,它用 functional 写法,让 "first-name" 原封不动地保留下来:

from typing import TypedDict

Root = TypedDict("Root", {"first-name": str, "2fa": bool})

结果总是可运行的,而在 TypedDict 模式下,key 与 API 发来的逐字节一致。

用生成的类解析 JSON

生成类只是一半的活。把 JSON 载入其中,才是三种模式分道扬镳之处,也是一个流行假设出错的地方。

dataclass。 对于扁平对象,json.loads 加上 Root(**data) 直接可用:

import json
from dataclasses import dataclass
from typing import List


@dataclass
class Root:
    id: int
    name: str
    roles: List[str]


data = json.loads('{"id": 101, "name": "Ada Lovelace", "roles": ["admin"]}')
root = Root(**data)
print(root.name, root.roles)

但 dataclass 不会递归。如果 JSON 里有嵌套对象,Root(**data) 只会填好顶层,把每个子对象都留成普通的 dict,而不是你生成的 Owner 类。要解析整棵树,就得自己构建子对象,或者用 dacite 这类库、pydantic.dataclasses,再不然把工具切到 Pydantic 模式。这正是大多数转换器从不提及的坑。

Pydantic v2。 一次调用就解析并校验整棵树:

import json
from pydantic import BaseModel


class Root(BaseModel):
    id: int
    name: str


root = Root.model_validate(json.loads('{"id": 101, "name": "Ada Lovelace"}'))
print(root.name)

类型不匹配会抛出一个清晰的 ValidationError,而不是静默放行。这就是那份依赖换来的回报:模型是一个运行时校验器,所以一次成功返回的 model_validate 调用,是数据确实匹配的保证,而不只是类型检查器一厢情愿地认定它匹配。

TypedDict。 运行时,TypedDict 就是一个普通的 dict。给 data: Root = json.loads(text) 加注解纯粹是给静态检查器看的;代码运行时不会校验任何东西。如果你需要真正的强制约束,就在 CI 里跑 mypy,或者转向 Pydantic。选模式其实就是在选你想要多少运行时保证;而当一份契约需要超越类型本身时,JSON Schema 校验完全指南讲了如何端到端地强制结构。

Pydantic v2 与 v1——变了什么

转换器输出的是 v2 语法。仍有大量代码库跑在 v1 上,而把 v2 输出粘进 v1 项目,报错出在改了名的方法上,而非逻辑上。对照如下:

任务Pydantic v2(工具输出)Pydantic v1
解析 dictmodel_validate(data)parse_obj(data)
解析原始 JSONmodel_validate_json(text)parse_raw(text)
配置模型model_config = ConfigDict(...)内部 class Config
序列化为 dictmodel_dump()dict()
序列化为 JSONmodel_dump_json()json()

别名的行为也收紧了:在 v2 里,你需要 populate_by_name=True 配合 Field(alias=...),才能用字段名或别名任一种方式构造模型,这也是工具把它加进去的原因。实用的规则很短:能升到 v2 就升,因为它更快,也是当前开发的方向;否则就把 model_validate 改回 parse_obj,把 ConfigDict 改回内部的 Config

json-to-python、quicktype、datamodel-code-generator 与手写的对比

从 JSON 生成 Python 类没有唯一最优解。这取决于 JSON 存在哪里、你手上又有什么可作起点。

方式最适合说明
在线转换器(本工具、jsonlint、codeshack)一次性转换、敏感 payload、零安装从样本推断,完全在客户端
quicktype多语言输出、流水线代码生成同样基于样本;在别名与模式选择上,Python 支持较薄弱
datamodel-code-generator从 JSON Schema 或 OpenAPI 生成 Pydantic输入是 schema 而非样本,有 schema 时更权威
手写类极小的 payload、学习语法完全掌控,但繁琐且容易漂移

区别在于:在线转换器和 quicktype 都从 JSON 样本推断类型,而 datamodel-code-generator 以 JSON Schema 或 OpenAPI 规格作为事实来源。探索和一次性任务用基于样本的工具;契约已经存在时,就用 schema 驱动的那个。如果你的代码库是 TypeScript 而非 Python,同样的样本驱动思路可用 JSON 转 TypeScript 实现,深入的一面见 JSON 转 TypeScript 接口指南。至于强类型编译语言里的运行时校验视角,JSON 转 Rust 结构体指南提供了一个有用的对照:serde 就是 Rust 的 Pydantic。

从 JSON 生成 Python 时的常见陷阱

生成的类只是起点。在用真实数据依赖这些输出之前,留意以下几点。

  • 单个样本很少能揭示全部形状。 Optional 只能从真正存在差异的数组元素里推断出来。粘一个有代表性的数组,可选性才准确,而不是从一个碰巧的对象里瞎猜。
  • dataclass 不做校验。 Root(**data) 只是给字段赋值;它不检查类型,也不递归任何东西。需要强制约束就用 Pydantic。
  • 嵌套 JSON 会让 Root(**data) 失效。 只有顶层被填充,子对象仍是 dict。改用 Pydantic 的 model_validate,或用 dacite。
  • 始终为 null 的字段会变成 Optional[Any] 把它当作占位符,而非普通的可选字段;一旦知道真实类型就替换上去。
  • v2 输出进了 v1 项目。 model_validate 会未定义。升级到 v2,或把它改写成 parse_obj
  • 留成 str 的日期没法做日期运算。 把字段换成 datetime,或在 Pydantic 里用 datetime 字段,让它替你解析 ISO 8601。

常见问题

针对 JSON,我该用 dataclass、Pydantic 还是 TypedDict?

内部数据持有者、且想零依赖,用 dataclass。解析或校验不可信输入,用 Pydantic v2,因为它在运行时强制类型。给手头已有的 dict 加静态提示、又不想要运行时开销,用 TypedDict。数据来自你控制不了的地方时,默认选 Pydantic。

Pydantic 会在运行时校验 JSON 吗?

会。Root.model_validate(data) 在代码运行时校验并强制转换类型,遇到坏输入就抛 ValidationError。dataclass 两样都不做,Root(**data) 只是给字段赋值。TypedDict 同样两样都不做;它是给 mypy 这类静态检查器看的提示,运行时的表现就是一个普通 dict。

JSON 有时会省略的字段,我该怎么定类型?

把它定为 Optional[T]。当某个 key 只出现在部分数组元素里时,转换器会自动把它标为 Optional。在 Pydantic 里,给该字段一个默认值,比如 = None,这样缺失 key 就不会触发校验错误。dataclass 字段也能用 field(default=None) 以同样方式设默认值。

为什么我的 dataclass 解析不了嵌套 JSON?

因为 Root(**data) 不递归。它只填好顶层字段,把嵌套对象留成普通 dict,而不是你生成的子类。要一次调用解析整棵树,改用 Pydantic 的 model_validate,或用 dacite、pydantic.dataclasses 给它补上递归。

很大的 JSON 整数会变成什么 Python 类型?

int。Python 整数是任意精度的,所以超过 2^53 的雪花 ID 或 Discord ID 照样是普通的 int,没有任何损失。你永远不会遇到 JavaScript 那样的精度丢失,也永远不用像 Rust 逼你的那样在 i64u64 之间做选择。

各模式下 camelCase 的 JSON key 是怎么处理的?

dataclass 保留原始 key,所以 Root(**data) 仍然可用。Pydantic 把字段改名为 snake_case,配 Field(alias="camelKey")populate_by_name=True,因此能 round-trip 真实的 API 数据。TypedDict 用 functional 的 TypedDict('Name', {...}) 语法保留任何不是合法标识符的 key。

JSON key 是像 class 这样的 Python 关键字,我该怎么处理?

转换器会追加一个尾部下划线,把 class 变成 class_,让代码合法。在 Pydantic 模式下,它还会加上 Field(alias="class"),把字段映射回原始的传输 key,这样字段名既合法,模型又仍能读到真实的 payload。

用在线 JSON 转 Python 转换器时,我的 JSON 私密吗?

私密。转换 100% 在你的浏览器里用 JavaScript 运行。你的 JSON——包括 token、ID 和客户数据——绝不离开页面,也绝不发往服务器。页面加载后,它甚至可以离线使用。准备好之后,就拿你自己的 payload 试试 JSON 转 Python 类转换器吧。

标签: python json pydantic type-safety developer-tools