登录
推荐 文章 Go 技术 课程 下载 专题 AI
首页 >  文章 >  php教程

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 响应。敏感字段不属于输出契约,应该在最初的字段选择阶段被排除。

领域对象、私有属性、passwordHash、jsonSerialize、公开字段数组、json_encode 与 JSON 响应的静态输出契约图
图1:JsonSerializable 输出字段契约结构图;重点看内部属性与公开字段数组之间的边界,图中连线只表示静态依赖。
设计项推荐做法原因
字段选择显式返回允许列表新增内部属性时不会自动泄露
字段命名以 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,再由 响应适配器 统一转换。

API 控制器、根响应对象、嵌套 View、json_encode、JSON_THROW_ON_ERROR、JsonException 与响应适配器的静态错误边界图
图2:嵌套对象与 JSON 错误处理的静态关系图;编码选项和异常转换集中在响应边界,不分散到各个领域对象。

兼容性取舍要由调用方需求决定

实现 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。

声明:本文转载于:17golang原创 如有侵犯,请联系study_golang@163.com删除
相关阅读
更多>
最新阅读
更多>
课程推荐
更多>