程序启动时读一份 JSON 配置,最怕的不是文件不存在,而是文件能打开、内容却少了一个字段,或者端口被写成了字符串。程序继续往下跑,等到真正用到这个值才报错,排查时已经离配置文件很远了。
下面的做法只使用 Python 标准库。当前文档以 Python 3.14 为准:json.loads 在文本不是合法 JSON 时抛出 json.JSONDecodeError,这个异常带有行号和列号。示例在 Python 3.14.3 里运行过。
先约定配置长什么样
一份本机服务配置可以是这样,保存为 app.json,编码用 UTF-8,不要带注释:
{
"name": "笔记站点",
"port": 8080,
"debug": false
}jsonJSON 没有注释语法。文件里出现 // 或 /* */,解析会直接失败。需要说明时,把说明写进旁边的文档,不要写进配置文件。
读取并检查字段
把下面的程序保存为 check_json_config.py,和 app.json 放在同一目录:
import json
from pathlib import Path
REQUIRED = {
"name": str,
"port": int,
"debug": bool,
}
def load_config(path: Path) -> dict:
try:
text = path.read_text(encoding="utf-8")
except FileNotFoundError:
raise SystemExit(f"找不到配置文件:{path}")
except UnicodeDecodeError as exc:
raise SystemExit(f"文件不是 UTF-8 文本:{exc}")
try:
data = json.loads(text)
except json.JSONDecodeError as exc:
raise SystemExit(
f"JSON 格式错误:第 {exc.lineno} 行,第 {exc.colno} 列,{exc.msg}"
)
if not isinstance(data, dict):
raise SystemExit("配置文件的最外层必须是对象,不能是数组或单个值")
missing = [key for key in REQUIRED if key not in data]
if missing:
raise SystemExit("缺少字段:" + "、".join(missing))
for key, expected in REQUIRED.items():
value = data[key]
if isinstance(value, bool) and expected is int:
raise SystemExit(f"字段 {key} 应该是整数,不能是 true 或 false")
if not isinstance(value, expected):
raise SystemExit(f"字段 {key} 的类型应为 {expected.__name__}")
if not 1 <= data["port"] <= 65535:
raise SystemExit(f"端口 {data['port']} 超出 1 到 65535 的范围")
return data
if __name__ == "__main__":
config = load_config(Path(__file__).with_name("app.json"))
print(f"配置可用:{config['name']},端口 {config['port']}")python在该目录执行:
python check_json_config.pybash配置正确时输出:
配置可用:笔记站点,端口 8080text这里有两个容易忽略的点。
Path.read_text(encoding="utf-8") 把编码说死。Windows 上不写编码时,会随系统区域设置变化,同一份含中文的文件换一台机器就可能读失败。
bool 是 int 的子类。如果只写 isinstance(True, int),结果是真。端口写成 true 时会混过去,所以要先把布尔值单独拒绝。
常见错误
最后一项后面多一个逗号,是手写 JSON 最常见的错误。解析器会报告类似:
第 3 行,第 15 列,Illegal trailing comma before end of objecttext其他几类也值得在启动时拦住:
| 文件内容 | 程序给出的结果 |
|---|---|
没有 port | 缺少字段:port |
"port": true | 字段 port 应该是整数,不能是 true 或 false |
"port": 70000 | 端口超出 1 到 65535 的范围 |
| 最外层是数组 | 最外层必须是对象 |
| 文件不存在 | 找不到配置文件 |
只想先确认括号和引号,可以用标准库自带的命令。它默认会把非英文字符转成 \u 形式,阅读中文时加上 --no-ensure-ascii:
python -m json.tool --no-ensure-ascii app.jsonbash命令能打印出排好的内容,只说明语法正确,不说明字段齐全。字段和取值范围仍要由程序自己检查。
不要用 eval 去读配置。JSON 里出现计算表达式或函数调用时,那已经不是配置文件该做的事,也会把不该执行的内容带进来。
类型和编码还会碰到的情况
数字并不都是整数。json.loads("1.5") 得到的是浮点数,isinstance(1.5, int) 为假,所以上面的端口检查会拒绝 "port": 1.5。如果某个字段确实要小数,期望类型就写成 float,同时接受整数,因为 JSON 的 1 会变成 int 而不是 float。
空值是 None,不是空字符串。配置里写 "name": null 时,类型检查会要求字符串,从而在启动时失败。这比读到一半才发现名字是空的要早。
文件开头如果带了编辑器加的 BOM,utf-8 解码会失败,错误信息里会出现 BOM。解决办法是把文件另存为不带 BOM 的 UTF-8,而不是在程序里悄悄忽略所有解码错误。忽略错误会把后面的中文读成乱码,配置仍能「打开」,内容却是错的。
字段多出来时,这份示例选择保留,不报错。名称写错时,例如把 port 写成 ports,缺少字段的检查仍会抓住真正要用的 port。如果配置越来越长,再把允许的字段列成白名单,多出来的键也一并提示,避免拼写错误被静默留下。
检查通过之后再把配置交给真正启动服务的代码。不要在业务逻辑里重复写一套 if "port" in data。校验集中在 load_config,后面的代码就可以认为这三个字段一定存在,而且类型正确。
小结
读取 JSON 配置时,把「文件打不开」「不是 UTF-8」「语法不对」「字段缺失」「类型不对」「取值越界」分成几步报出来。错误出在启动阶段,并带上行号,比服务跑起来以后再查要直接得多。