一、先弄清JSON与Python的类型映射
JSON是跨语言的数据交换格式,Python标准库的json模块负责在两者之间做双向转换。转换规则是固定的,记牢这张对应表可以避免大部分“类型对不上”的低级错误:
- # JSON类型 -> Python类型
- # object -> dict
- # array -> list
- # string -> str
- # number(int) -> int
- # number(real) -> float
- # true / false -> True / False
- # null -> None
复制代码
反向序列化时,dict、list、str、int、float、bool、None都能直接写出;而datetime、date、自定义类实例、set等类型不在JSON规范内,这是后文TypeError的根源。
二、四个核心函数的分工
json模块对外最常用的只有四个函数,按“操作对象”划分:dumps()/loads() 处理字符串,dump()/load() 处理文件句柄。两者参数基本一致,只是后者多了一个文件对象参数。
2.1 json.dumps():Python对象转JSON字符串
- import json
- data = {
- "name": "张三",
- "age": 25,
- "languages": ["Python", "JavaScript"],
- "active": True,
- "score": None,
- "address": {"city": "北京", "district": "海淀"},
- }
- # 默认输出会把中文转成\uXXXX,加 ensure_ascii=False 才能保留中文
- json_str = json.dumps(data, ensure_ascii=False)
- print(json_str)
- # 紧凑格式:separators 指定元素间与键值间的分隔符
- compact = json.dumps(data, ensure_ascii=False, separators=(",", ":"))
- print(compact)
- # 美化输出:indent 指定缩进空格数
- pretty = json.dumps(data, ensure_ascii=False, indent=2)
- print(pretty)
- # 键排序:sort_keys=True,便于做diff比对或生成稳定签名
- sorted_keys = json.dumps(data, ensure_ascii=False, indent=2, sort_keys=True)
复制代码
几个参数的适用场景值得单独说:ensure_ascii=False 用于任何要给人看或要落盘的中文数据;separators 用于网络传输,去掉多余空白能减小体积;indent 用于配置文件,方便人工维护;sort_keys 用于需要输出稳定文本的场合,比如接口签名或版本控制下的配置比对。
2.2 json.loads():JSON字符串转Python对象
- import json
- json_str = '''
- {
- "name": "李四",
- "age": 30,
- "skills": ["Python", "Django", "SQL"],
- "is_admin": true,
- "profile": null
- }
- '''
- data = json.loads(json_str)
- print(f"姓名: {data['name']}")
- print(f"年龄: {data['age']}")
- print(f"技能: {', '.join(data['skills'])}")
- print(f"管理员: {data['is_admin']}") # JSON的true -> Python的True
- print(f"头像: {data['profile']}") # JSON的null -> Python的None
复制代码
注意JSON里的true/null在前端和Python里写法不同,反序列化后自动变成True/None,不要再按字符串"true"去判断。
2.3 json.dump()与json.load():直接读写文件
- import json
- config = {
- "app_name": "MyApp",
- "version": "1.0.0",
- "database": {
- "host": "localhost",
- "port": 5432,
- },
- "features": ["auth", "logging", "api"],
- }
- # 写入文件
- with open("config.json", "w", encoding="utf-8") as f:
- json.dump(config, f, ensure_ascii=False, indent=2)
- # 从文件读取
- with open("config.json", "r", encoding="utf-8") as f:
- loaded = json.load(f)
- print(loaded["app_name"]) # MyApp
- print(loaded == config) # True
复制代码
文件读写务必显式指定encoding="utf-8",否则在Windows默认编码下写中文可能抛UnicodeEncodeError,或读回时出现乱码。
三、非标准类型:datetime为什么报TypeError
json模块只认识上表中的七种类型。直接序列化datetime会抛异常:
- import json
- from datetime import datetime
- # json.dumps({"time": datetime.now()}) # TypeError: Object of type datetime is not JSON serializable
复制代码
报错信息里已点明原因:datetime不是可序列化类型。json模块在遇到未知类型时会回调default()方法,默认实现直接抛出上面的TypeError。因此解决思路就是接管这个回调,有下面三种写法。
方案一:自定义JSONEncoder子类
- import json
- from datetime import datetime, date
- class DateTimeEncoder(json.JSONEncoder):
- def default(self, obj):
- if isinstance(obj, (datetime, date)):
- return obj.isoformat()
- return super().default(obj)
- data = {
- "user": "张三",
- "created_at": datetime.now(),
- "birthday": date(1995, 8, 20),
- }
- json_str = json.dumps(data, cls=DateTimeEncoder, ensure_ascii=False)
- print(json_str)
复制代码
这种方式适合项目中多处需要序列化时间字段的场景,编码器可以复用,也便于统一时间格式。
方案二:传default函数
- import json
- from datetime import datetime, date
- def custom_serializer(obj):
- if isinstance(obj, datetime):
- return obj.isoformat()
- if isinstance(obj, date):
- return obj.isoformat()
- raise TypeError(f"无法序列化类型: {type(obj)}")
- json_str = json.dumps(data, default=custom_serializer, ensure_ascii=False)
复制代码
适合一次性、临时性的序列化需求。注意未知类型要主动raise TypeError,否则会被静默吞掉,排查时反而更难定位。
方案三:序列化前先转成基础类型
- data_serializable = {
- "user": "张三",
- "created_at": datetime.now().isoformat(),
- "birthday": date(1995, 8, 20).isoformat(),
- }
- json_str = json.dumps(data_serializable, ensure_ascii=False)
复制代码
这是最容易理解、最不依赖json模块特性的做法,缺点是数据一旦嵌套较深,手工转换的代码会变得啰嗦且容易漏字段。
四、自定义解码:object_hook与object_pairs_hook
反序列化时同样可以把特定字段还原成Python对象,入口是object_hook——它会在每个dict构造完成后被调用。
- import json
- from datetime import datetime
- json_str = '''
- {
- "user": "张三",
- "created_at": "2024-06-15T14:30:00",
- "login_count": "100"
- }
- '''
- def custom_decoder(dct):
- """自动将isoformat时间字符串转为datetime"""
- result = {}
- for key, value in dct.items():
- if isinstance(value, str):
- try:
- result[key] = datetime.fromisoformat(value)
- continue
- except (ValueError, TypeError):
- pass
- result[key] = value
- return result
- data = json.loads(json_str, object_hook=custom_decoder)
- print(f"类型: {type(data['created_at'])}") # <class 'datetime.datetime'>
复制代码
这段实现有一个必须注意的副作用:它对所有字符串都尝试解析时间,普通文本只要能被fromisoformat识别就会被误转换。生产中更稳妥的做法是按字段名白名单判断,例如只处理key为created_at、updated_at这类字段。
如果只是想让字典保持JSON中的原始键顺序,可以用object_pairs_hook:
- data_ordered = json.loads(json_str, object_pairs_hook=dict)
复制代码
五、类对象与嵌套数据的序列化
json模块不会自动识别自定义类,需要在类上自己暴露to_dict()/from_dict(),把对象和字典之间的转换显式化。
- import json
- class User:
- def __init__(self, name, email, roles=None):
- self.name = name
- self.email = email
- self.roles = roles or []
- def to_dict(self):
- return {"name": self.name, "email": self.email, "roles": self.roles}
- @classmethod
- def from_dict(cls, data):
- return cls(data["name"], data["email"], data.get("roles", []))
- def to_json(self):
- return json.dumps(self.to_dict(), ensure_ascii=False)
- # 序列化
- user = User("张三", "zs@test.com", ["admin", "editor"])
- json_str = user.to_json()
- print(json_str)
- # 反序列化
- data = json.loads(json_str)
- user2 = User.from_dict(data)
- print(user2.name, user2.email, user2.roles)
复制代码
这种写法的好处是:序列化边界清晰,嵌套对象只需在to_dict()里递归展开;from_dict()用data.get("roles", [])给可选字段兜底,避免旧数据缺字段时直接KeyError。
六、大文件流式解析:raw_decode逐条读取
json.load()会把整个文件一次性载入内存,几百MB的JSON数组很容易把内存吃满。这种情况下可以用JSONDecoder.raw_decode()做增量解析,边读边产出对象。
- import json
- def stream_parse_json_array(filepath):
- """流式解析JSON数组——逐条产出对象"""
- with open(filepath, "r", encoding="utf-8") as f:
- # 跳过开头的 [
- char = f.read(1)
- while char and char != '[':
- char = f.read(1)
- decoder = json.JSONDecoder()
- buffer = ""
- for line in f:
- line = line.strip()
- if line in (']', '],'):
- break # 数组结束
- if line.endswith(','):
- line = line[:-1]
- buffer += line
- try:
- obj, _ = decoder.raw_decode(buffer)
- yield obj
- buffer = ""
- except json.JSONDecodeError:
- continue # 数据不完整,等待下一行
复制代码
构造测试数据验证一下:
- with open("large.json", "w") as f:
- f.write("[\n")
- for i in range(5):
- f.write(f' {{"id": {i}, "name": "item{i}"}}')
- f.write(",\n" if i < 4 else "\n")
- f.write("]")
- for item in stream_parse_json_array("large.json"):
- print(item)
复制代码
这套实现的前提是“每行一个JSON对象”,即常说的JSON Lines排版风格。如果源文件是整行压缩成一个巨型数组的单行JSON,逐行读的逻辑就不成立,需要改造成按字符块累加buffer再尝试raw_decode。另外raw_decode返回的是(对象, 结束位置)二元组,本例丢弃了结束位置,只适用于一行恰好一个对象的场景。
七、结构校验:不引第三方库的轻量方案
很多场景不需要完整的JSON Schema,只要确认必填字段在不在、类型对不对。写个几十行的校验函数就够了。
- import json
- def validate_json_structure(data, required_fields, field_types=None):
- """验证JSON数据的结构"""
- errors = []
- for field in required_fields:
- if field not in data:
- errors.append(f"缺少必填字段: {field}")
- if field_types:
- for field, expected_type in field_types.items():
- if field in data and not isinstance(data[field], expected_type):
- errors.append(
- f"字段{field}类型错误: "
- f"期望{expected_type.__name__}, "
- f"实际{type(data[field]).__name__}"
- )
- return len(errors) == 0, errors
- user_schema = {
- "required_fields": ["name", "email"],
- "field_types": {"name": str, "email": str, "age": int},
- }
- data = {"name": "张三", "email": "zs@test.com", "age": "二十五"}
- valid, errors = validate_json_structure(
- data,
- user_schema["required_fields"],
- user_schema["field_types"]
- )
- print(f"验证: {'通过' if valid else '失败'}")
- for e in errors:
- print(f" - {e}")
复制代码
这个函数返回布尔值和错误列表,便于在接口层统一汇总报错。要留意isinstance对bool的判断:Python中bool是int的子类,若字段期望int而实际传入True,校验会通过,需要额外排除。
八、实践要点汇总
1. dumps()/loads() 面向字符串,dump()/load() 面向文件对象,参数含义一致。
2. ensure_ascii=False 才能输出可读中文;indent=2 用于美化;separators=(",", ":") 用于压缩;sort_keys=True 用于输出稳定文本。
3. datetime/date 等类型无法直接序列化,报错信息为“not JSON serializable”,用JSONEncoder子类、default回调或预先isoformat()转换三种方式解决。
4. 反序列化还原时间字段用object_hook,但要按字段名限定,避免把普通字符串误转成datetime。
5. 自定义类通过to_dict()/from_dict()显式转换,可选字段用dict.get()兜底。
6. 超大JSON数组用JSONDecoder.raw_decode()流式处理,前提是文件为逐行对象格式。
7. 字段与类型校验可以自己实现,不必为简单场景引入Schema依赖。
JSON在API交互、配置落盘、数据传输中几乎无处不在,json模块本身足够用,真正需要额外投入精力的地方在于非标准类型的边界处理和内存占用控制。 |