`json_stream`: objects inside a list stay uninitialized lazy ghosts, so `#[Assert\Valid]` validates them as blank
维护者通常 2 天内回复
还没有人认领这个 Issue。
评估
- 难度
- 4/5
- 预计耗时
- 3-5 天
- 新手友好度
- 48/100
- Issue 类型
- 缺陷
- 描述清晰度
- 描述清楚
- 活跃度
- 活跃
调研方向
Start in JsonStreamerProvider and trace how streamed objects in list properties reach ValidateProvider; compare this path with the non-streaming Serializer path. Check the relevant provider and validation tests, then add a regression case for a DTO list with #[Assert\Valid] and #[Assert\NotBlank]. Done means valid list items pass validation and invalid ones still produce violations.
由索引模型根据 Issue 内容生成。
描述
API Platform version(s) affected: 5.0.5 (api-platform/symfony, api-platform/serializer); the same code is on main
Environment: PHP 8.5.4, symfony/json-streamer 8.1.10, symfony/validator 8.1.11
Description
When an operation uses jsonStream: true, ApiPlatform\Serializer\State\JsonStreamerProvider reads the request body with StreamReaderInterface::read($request->getContent(true), …). Because the input is a stream, JsonStreamer builds the objects through its LazyInstantiator (PHP lazy ghosts). The provider then initializes only the top-level object:
if (\PHP_VERSION_ID > 80400) {
$refl = new \ReflectionClass($data);
if ($refl->isUninitializedLazyObject($data)) {
$refl->initializeLazyObject($data);
}
}
Objects inside a list<…> property are still uninitialized ghosts when ValidateProvider runs. The Validator reads properties through PropertyMetadata::getPropertyValue(), which returns null when ReflectionProperty::isInitialized() is false. On an uninitialized ghost, isInitialized() returns false and does not trigger initialization. So every constraint on a list item sees an empty value, and a valid payload is rejected with 422.
A single nested object property (public ?Line $line) is not affected in my tests: only objects that are list items are.
How to reproduce
<?php
namespace App\ApiResource;
use ApiPlatform\Metadata\ApiResource;
use ApiPlatform\Metadata\Post;
use Symfony\Component\Validator\Constraints as Assert;
#[ApiResource(operations: [new Post(uriTemplate: '/repro/orders', processor: [self::class, 'echo'])], jsonStream: true)]
final class ReproOrder
{
/** @var list<ReproLine> */
#[Assert\Valid]
public array $lines = [];
public static function echo(self $data): self
{
return $data;
}
}
final class ReproLine
{
#[Assert\NotBlank]
public string $label = '';
}
with formats: { json: ['application/json'] } in api_platform.yaml, then:
curl -X POST http://localhost/api/repro/orders \
-H 'Content-Type: application/json' -H 'Accept: application/json' \
-d '{"lines":[{"label":"A"},{"label":"B"}]}'
Expected: 201 {"lines":[{"label":"A"},{"label":"B"}]}
Actual:
422 {"status":422,"violations":[
{"propertyPath":"lines[0].label","message":"This value should not be blank.", …},
{"propertyPath":"lines[1].label","message":"This value should not be blank.", …}
], …}
The same request without jsonStream: true (Serializer path) returns 201.
Possible Solution
Initialize the whole object graph in JsonStreamerProvider after reading, not just the root. Here is a userland workaround that fixes it: a decorator placed between the JSON streamer provider (priority 310) and deserialize/validate (300/200):
#[AsDecorator('api_platform.state_provider.main', priority: 305)]
final readonly class InitializeStreamedObjects implements ProviderInterface
{
public function __construct(#[AutowireDecorated] private ProviderInterface $inner)
{
}
public function provide(Operation $operation, array $uriVariables = [], array $context = []): object|array|null
{
$data = $this->inner->provide($operation, $uriVariables, $context);
if (($context['request'] ?? null)?->attributes->get('deserialized')) {
self::initialize($data);
}
return $data;
}
private static function initialize(mixed $value): void
{
if (is_array($value)) {
array_walk($value, static fn (mixed $item) => self::initialize($item));
return;
}
if (!is_object($value)) {
return;
}
$class = new \ReflectionClass($value);
$class->initializeLazyObject($value);
array_map(static fn (\ReflectionProperty $property) => self::initialize($property->isInitialized($value) ? $property->getValue($value) : null), $class->getProperties(\ReflectionProperty::IS_PUBLIC));
}
}
Alternatively, the Validator could treat uninitialized lazy objects specially, though that seems a wider change than fixing it where API Platform hands the object over.
Additional Context
Found while moving an app's write endpoints to json_stream: true with nested DTO lists (a check-list editor PUTting lists of items).
- 主要语言
- PHP
- 星标
- 2.6k
- 派生
- 988
- 平均合并
- 4 天 10 小时
- 30 天内合并 PR
- 114
环境准备
- 没有 Dockerfile 或 Docker Compose 文件
- 有 Pull Request 模板
- 阅读贡献指南
从这里开始
- 先读完整个 Issue,再读项目的贡献指南。
- 在 Issue 下留言说明你要接手 —— 这能避免两个人做同样的事。
- Fork 仓库,在一个分支上完成修改。
- 提交 Pull Request,并在描述里引用这个 Issue 编号。
api-platform/core 的其他 Issue
-
API Platform ignores array shapes in the JSON Schema可能已有人在做 @afraca 于 11 天前认领。 未关闭
难度 2/5 1-3 小时 新手友好度 78/100
api-platform/core#8766 ·
维护者通常 2 天内回复
-
难度 2/5 1-3 小时 新手友好度 72/100
api-platform/core#8744 ·
维护者通常 2 天内回复
-
JSON:API: resource schemas (`*.jsonapi`) have `properties` but no `type: object`可能已有人在做 关联的 PR 仍在进行中或已合并。 未关闭
难度 2/5 1 小时以内 新手友好度 80/100
api-platform/core#8701 ·
维护者通常 2 天内回复
-
难度 4/5 3-5 天 新手友好度 55/100
api-platform/core#8816 ·
维护者通常 2 天内回复
-
难度 3/5 1-2 天 新手友好度 66/100
api-platform/core#8815 ·
维护者通常 2 天内回复
查看 api-platform/core 的全部 Issue
相似的 Issue
-
难度 2/5 1-3 小时 新手友好度 78/100
endoflife-date/endoflife.date#11561 ·
维护者通常 1 天内回复
-
good first issue priority:low type:bug
难度 2/5 1-3 小时 新手友好度 78/100
crazy-goat/fdb-php#184 ·
维护者通常 1 天内回复
-
难度 2/5 1-3 小时 新手友好度 75/100
nextcloud/groupfolders#5218 ·
维护者通常 1 天内回复
-
难度 2/5 1-3 小时 新手友好度 78/100
nextcloud/sharelisting#624 ·
-
`wp theme mod set` prints its failure message as `Success` and exits 0可能已有人在做 @dhrupo 今天认领。 未关闭bug command:theme-mod-set status:unconfirmed
难度 2/5 1-3 小时 新手友好度 82/100
wp-cli/extension-command#570 ·