API Platform DTO Validator:缺失属性未触发校验违规问题
项目中使用API Platform的DataTransformer,注入ValidatorInterface校验POST请求传入的JSON请求体,实现逻辑参考官方文档示例,核心代码如下:
DataTransformer实现代码
<?php namespace App\DataTransformer; use ApiPlatform\Core\Validator\ValidatorInterface; use ApiPlatform\Core\DataTransformer\DataTransformerInterface; use App\Entity\Ticket; use App\DTO\TicketInput; class InputDataTransformer implements DataTransformerInterface { public function __construct( private readonly ValidatorInterface $validator, ) {} /** * @param TicketInput $object */ public function transform($object, string $to, array $context = []): Ticket { // 执行校验 $this->validator->validate($object); $ticket = new Ticket($object->id, $object->content); return $ticket; } public function supportsTransformation($data, string $to, array $context = []): bool { return (Ticket::class === $to) && (($context['input']['class'] ?? null) === TicketInput::class); } }
对应DTO定义
通过ContextBuilder与supportsTransformation()方法可保证传入的$object为TicketInput类型,DTO类代码如下:
<?php namespace App\DTO; class TicketInput { public int $id; public ?string $content; }
按照类型定义,预期校验规则为:
$id必须为整型,不可为null$content为字符串类型,允许为null
异常表现
以下两类输入可正常通过校验:
{ "id": 123, "content": null }
{ "id": 123, "content": "testtest" }
传入id为null的请求时,会被正确拦截并返回400 Bad Request错误:
{ "id": null, "content": "foobar" }
但请求体完全缺失id字段时,校验会直接通过:
{ "content": "foobar" }
此时$id属性处于未初始化状态,后续实例化Ticket类时会直接报错,因为Ticket要求id为必填值。
目前已知临时方案是给每个必填属性添加#[Assert\NotBlank(allowNull: false)]约束,但DTO字段多、逻辑复杂时逐个添加注解维护成本过高,需要无需逐字段加注解、可全局生效的配置,实现JSON请求体必填字段自动校验的能力。
出现该问题的核心原因:PHP类型声明仅校验属性赋值后的值是否匹配类型,不会校验属性是否完成初始化;而Symfony Serializer组件默认在反序列化JSON到DTO对象时,会跳过请求体中不存在的字段,不会给对应属性赋默认值,未初始化的属性默认不会触发Validator组件的类型校验。
不需要逐字段添加#[Assert\NotBlank]注解,有两种开箱可用的落地方案:
方案1:通过构造函数约束必填字段(推荐)
给DTO类添加构造函数,将所有必填字段定义为构造函数的必填参数,从反序列化环节直接拦截缺失字段的请求:
<?php namespace App\DTO; class TicketInput { public function __construct( public int $id, public ?string $content = null, ) {} }
API Platform默认使用的Symfony Serializer在反序列化时,会自动将请求体字段映射到构造函数参数,如果缺失必填的构造参数,会直接抛出400 Bad Request错误,不需要额外添加校验注解,类型不匹配的问题也会在反序列化阶段直接抛出。如果使用PHP 8.1及以上版本,可以给构造函数参数加上readonly关键字提升属性不可变性,逻辑不受影响。
方案2:全局修改Serializer配置拦截缺失字段
如果不想调整现有DTO的写法,可以修改Serializer全局配置,关闭缺失字段丢弃逻辑,让原有类型校验自动生效:
# config/packages/framework.yaml framework: serializer: default_context: discard_missing_fields: false
开启该配置后,请求体中缺失的字段不会被Serializer跳过,会被默认赋值为null,此时DTO中定义的非nullable类型属性(比如public int $id)接收到null值时,会被Validator组件直接检测到类型不匹配,返回400错误,不需要添加任何额外注解。该配置对已经设置默认值、或声明为nullable类型的可选字段无影响,不会产生额外副作用。
如果需要更细粒度的控制,可以配合API Platform的OpenAPI上下文配置,自动在生成的接口文档中标记必填字段,和实际校验规则保持一致。
内容的提问来源于stack exchange,提问作者Cologne_Muc

