`json_stream`: objects inside a list stay uninitialized lazy ghosts, so `#[Assert\Valid]` validates them as blank
メンテナーはふだん 2 日以内に返信
まだ誰も着手していません。
評価
- 難易度
- 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時間
- マージ済み PR(30日)
- 114
環境構築
- Dockerfile・Docker Compose ファイルなし
- プルリクエストのテンプレートあり
- コントリビューションガイドを読む
はじめの一歩
- issue を最後まで読み、次にプロジェクトのコントリビューションガイドを読みます。
- 着手することを issue にコメントします — 二人が同じ作業をするのを防げます。
- リポジトリをフォークし、ブランチを切って変更します。
- 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`対応中かも このイシューにリンクされたプルリクエストがオープン中、またはマージ済みです。 オープン
難易度 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
-
Visual COBOL: missing 11.0 and 12.0, and 9.0 date differs from Rocket Software's lifecycle pageオープン
難易度 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 ·