PHP JsonSerializable 控制对象输出字段
来源:17golang原创
时间:2026-10-01 21:11:42 129浏览 收藏
JsonSerializable 的作用,是让对象在传给 json_encode() 时主动声明“哪些字段属于 JSON 输出契约”。默认情况下,PHP 对普通对象只编码公开属性;实现该接口后,可以从私有属性中挑选字段、改名、组合派生值,并排除密码摘要、内部状态等不应暴露的数据。
最小做法是让类实现 JsonSerializable,然后在 jsonSerialize(): mixed 中返回一个可被 json_encode() 原生编码的值。API 场景通常返回关联数组,并在最外层编码时加入 JSON_THROW_ON_ERROR,让非法 UTF-8、递归引用或不支持的类型以异常形式进入统一错误处理。
官方地址:https://www.php.net/manual/en/class.jsonserializable.php
jsonSerialize()决定对象的 JSON 表示,不改变对象本身的属性可见性。- 方法可以返回数组、标量、对象或 null,但不能返回 resource;API 通常用关联数组保持字段结构明确。
- 敏感字段应采用“允许列表”显式输出,不要先导出全部属性再删除。
先用最小写法固定输出字段
下面的 UserProfile 保存了数据库主键、显示名称、邮箱、密码摘要和启用状态,但 JSON 只需要面向调用方提供稳定的公开表示。私有属性仍保持封装,jsonSerialize() 只返回允许暴露的字段。
$this->id,
'display_name' => $this->displayName,
'email' => $this->email,
'active' => $this->active,
];
}
}
$profile = new UserProfile(
42,
'林海',
'linhai@example.test',
'$2y$10$internal-only',
true,
);
// 统一在响应边界编码,失败时抛出 JsonException。
$json = json_encode(
$profile,
JSON_UNESCAPED_UNICODE | JSON_THROW_ON_ERROR,
);
echo $json;
结果结构包含 id、display_name、email 和 active。passwordHash 没有出现在返回数组里,因此不会被编码。这里不是利用 private 自动隐藏敏感值,而是通过允许列表把接口契约写死;即使以后属性改成 public,也不会意外扩大响应。
把对象模型与 API 字段契约分开
领域对象的字段名服务于内部代码,JSON 字段名服务于外部调用方,两者不必完全一致。比如 PHP 属性使用 $displayName,API 可以稳定输出 display_name;对象内部保存 DateTimeImmutable,API 则输出带时区的文本。这样数据库重构、属性可见性调整或内部类型替换,不必直接破坏客户端。
静态关系上,领域对象保留 私有属性 与 passwordHash,jsonSerialize() 只组装 公开字段数组,最后由 json_encode() 生成 JSON 响应。敏感字段不属于输出契约,应该在最初的字段选择阶段被排除。

| 设计项 | 推荐做法 | 原因 |
|---|---|---|
| 字段选择 | 显式返回允许列表 | 新增内部属性时不会自动泄露 |
| 字段命名 | 以 API 契约为准 | 避免客户端依赖 PHP 内部命名 |
| 日期时间 | 格式化为约定文本 | 调用方无需理解 PHP 对象结构 |
| 错误处理 | 外层使用 JSON_THROW_ON_ERROR | 避免把 false 当成合法响应 |
处理别名、派生字段和可空值
jsonSerialize() 不必逐字复制对象属性。它可以添加派生字段、重命名字段,也可以根据契约决定 null 是保留还是省略。关键是行为要稳定:同一个资源的同一个接口,不应因为某次属性为空就随意改变数据类型。
$this->number,
'amount' => [
'value' => $this->amountCents,
'unit' => 'cent',
'currency' => $this->currency,
],
// 未支付时明确返回 null,客户端字段形状保持稳定。
'paid_at' => $this->paidAt?->format(DATE_ATOM),
];
}
}
金额字段没有直接转换为小数,而是同时输出整数值、单位与币种,这属于接口设计决定。paid_at 在未支付时保留为 null,调用方可以区分“尚未支付”与“字段在当前版本不存在”。如果业务选择省略空值,也应集中在一个明确的响应映射层处理,而不是让不同类各自随意过滤。
嵌套对象不需要手动调用 jsonSerialize
当返回数组中还包含另一个实现 JsonSerializable 的对象时,可以直接把对象放入数组,交给 json_encode() 递归编码。不要在父对象中手动调用子对象的 jsonSerialize();手动调用会绕过统一编码边界,也让测试与错误处理更分散。
$this->city,
'street' => $this->street,
];
}
}
final class CustomerView implements JsonSerializable
{
public function __construct(
private int $id,
private AddressView $address,
) {
}
public function jsonSerialize(): mixed
{
return [
'id' => $this->id,
'address' => $this->address, // 由 json_encode 递归处理子对象。
];
}
}
集合也遵循相同规则:返回 CustomerView[] 数组后,json_encode() 会逐个处理。需要注意循环引用,例如父对象返回子对象、子对象又返回父对象,会触发递归错误。输出 View 最好只保留单向、无环的响应结构。
把编码错误集中到响应边界
PHP 官方文档说明,jsonSerialize() 可以返回任何能被 json_encode() 原生处理的值,但 resource 不受支持;所有字符串还必须是有效 UTF-8。若沿用默认错误行为,json_encode() 失败会返回 false,调用方很容易遗漏检查。PHP 7.3 起可使用 JSON_THROW_ON_ERROR,失败时抛出 JsonException。
不要同时依赖 JSON_PARTIAL_OUTPUT_ON_ERROR 与 JSON_THROW_ON_ERROR 来获得严格失败,因为官方常量说明中,前者优先。对 API 来说,静默用 null 或 0 替换不可编码值可能掩盖数据问题;更稳妥的策略是让异常进入统一响应适配器,并返回不含内部细节的错误信息。
这套静态关系由 API 控制器 持有 根响应对象,根对象可包含 嵌套 View;json_encode() 使用 JSON_THROW_ON_ERROR,失败进入 JsonException,再由 响应适配器 统一转换。

兼容性取舍要由调用方需求决定
实现 JsonSerializable 后,字段变化就是 API 变化。删除字段、改名或把字符串改成对象,都会影响客户端;新增可选字段通常更容易兼容,但仍要考虑严格 schema 校验。大型项目可以让领域对象保持纯净,再建立专门的 UserView、InvoiceView 或响应 DTO 实现接口,以免同一个领域对象被多个接口迫使承担不同 JSON 形状。
- 同一对象只有一种公开表示:直接在对象上实现
JsonSerializable较简洁。 - 不同接口需要不同字段:建立独立 View/DTO,避免用全局状态或当前用户身份改变
jsonSerialize()结果。 - 需要字段版本:在响应构造层选择 V1/V2 View,不要让一个方法根据隐式环境返回两套形状。
- 支持较旧 PHP:
mixed返回类型需要 PHP 8.0;面向更早版本时应按目标运行时调整声明并做兼容测试。
JsonSerializable 只控制 json_encode() 的 JSON 表示,它与 PHP 的 Serializable、__serialize() 和 serialize() 不是同一套机制。前者服务跨语言 JSON 契约,后者服务 PHP 值的可存储表示,不能互相替代。
完整示例:返回稳定的用户详情响应
下面把字段允许列表、日期格式、嵌套对象和异常编码组合到一起。控制器只负责创建响应对象并调用统一编码器,具体 JSON 字段由 View 自己声明。
$this->code,
'label' => $this->label,
];
}
}
final class UserDetailView implements JsonSerializable
{
/** @param list $roles */
public function __construct(
private int $id,
private string $displayName,
private DateTimeImmutable $createdAt,
private array $roles,
private string $internalNote,
) {
}
public function jsonSerialize(): mixed
{
// internalNote 是内部字段,故意不进入公开响应。
return [
'id' => $this->id,
'display_name' => $this->displayName,
'created_at' => $this->createdAt->format(DATE_ATOM),
'roles' => $this->roles,
];
}
}
$response = new UserDetailView(
42,
'林海',
new DateTimeImmutable('2026-09-01T08:30:00+08:00'),
[new RoleView('editor', '内容编辑')],
'仅供内部审核使用',
);
try {
// HTTP 层可把这里的字符串写入响应体,并设置 application/json。
echo json_encode(
$response,
JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES | JSON_THROW_ON_ERROR,
);
} catch (JsonException $error) {
// 实际项目应交给统一异常处理中间件生成错误响应。
http_response_code(500);
echo '{"error":"响应编码失败"}';
}
检查这段设计时,只需确认四件事:内部字段没有进入允许列表;字段名称符合 API 约定;嵌套对象也实现了清晰契约;编码错误不会以 false 混入正常响应。这样对象内部结构可以继续演进,而客户端看到的 JSON 保持稳定。
常见问题
jsonSerialize 必须返回数组吗?
不必须。官方签名返回 mixed,可以返回可被 json_encode() 编码的数组、对象、标量或 null,但不能返回 resource。API 对象通常返回关联数组,因为字段语义最清楚。
private 属性会自动进入 JSON 吗?
普通对象默认只编码公开属性。实现 JsonSerializable 后,最终输出由 jsonSerialize() 的返回值决定,因此私有属性只有在方法显式放入返回值时才会出现。
可以在 jsonSerialize 中根据当前登录用户隐藏字段吗?
技术上可以读取外部状态,但不推荐。序列化结果会变得难以预测和测试。更清楚的方式是在响应构造层选择不同的 View 或字段集合,再让每个 View 保持确定输出。
为什么推荐 JSON_THROW_ON_ERROR?
它会在编码失败时抛出 JsonException,便于统一错误处理,避免遗漏 json_encode() 返回 false 的分支。需要严格失败时,不要再混用优先级更高的 JSON_PARTIAL_OUTPUT_ON_ERROR。
-
332 收藏
-
329 收藏
-
377 收藏
-
141 收藏
-
203 收藏
-
292 收藏
-
409 收藏
-
357 收藏
-
278 收藏
-
176 收藏
-
485 收藏
-
201 收藏
-
409 收藏
-
394 收藏
-
263 收藏
-
237 收藏
-
125 收藏
-
- 前端进阶之JavaScript设计模式
- 设计模式是开发人员在软件开发过程中面临一般问题时的解决方案,代表了最佳的实践。本课程的主打内容包括JS常见设计模式以及具体应用场景,打造一站式知识长龙服务,适合有JS基础的同学学习。
- 立即学习 543次学习
-
- GO语言核心编程课程
- 本课程采用真实案例,全面具体可落地,从理论到实践,一步一步将GO核心编程技术、编程思想、底层实现融会贯通,使学习者贴近时代脉搏,做IT互联网时代的弄潮儿。
- 立即学习 516次学习
-
- 简单聊聊mysql8与网络通信
- 如有问题加微信:Le-studyg;在课程中,我们将首先介绍MySQL8的新特性,包括性能优化、安全增强、新数据类型等,帮助学生快速熟悉MySQL8的最新功能。接着,我们将深入解析MySQL的网络通信机制,包括协议、连接管理、数据传输等,让
- 立即学习 500次学习
-
- JavaScript正则表达式基础与实战
- 在任何一门编程语言中,正则表达式,都是一项重要的知识,它提供了高效的字符串匹配与捕获机制,可以极大的简化程序设计。
- 立即学习 487次学习
-
- 从零制作响应式网站—Grid布局
- 本系列教程将展示从零制作一个假想的网络科技公司官网,分为导航,轮播,关于我们,成功案例,服务流程,团队介绍,数据部分,公司动态,底部信息等内容区块。网站整体采用CSSGrid布局,支持响应式,有流畅过渡和展现动画。
- 立即学习 485次学习