`json_stream`: objects inside a list stay uninitialized lazy ghosts, so `#[Assert\Valid]` validates them as blank

Đang mở
#8,819 0 bình luận 0 reaction 0 người được giao Xem trên GitHub

Maintainer thường phản hồi trong vòng 2 ngày

Chưa có ai nhận issue này.

Đánh giá

Độ khó
4/5
Thời gian dự kiến
3-5 ngày
Mức phù hợp với người mới
48/100
Loại issue
Lỗi
Độ rõ ràng
Đặc tả rõ ràng
Mức độ hoạt động
Sôi nổi
Công nghệ
php, symfony

Hướng nghiên cứu

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.

Do mô hình lập chỉ mục viết ra từ nội dung của issue.

Mô tả

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).

Ngôn ngữ chính
PHP
Star
2.6k
Fork
988
Merge trung bình
4 ngày 10 giờ
Pull request đã merge (30 ngày)
114

Chuẩn bị môi trường

Bắt đầu từ đâu

  1. Đọc hết issue, rồi đọc hướng dẫn đóng góp của dự án.
  2. Bình luận trên issue rằng bạn sẽ nhận — tránh hai người làm cùng một việc.
  3. Fork repository và làm thay đổi trên một nhánh.
  4. Mở pull request có tham chiếu số hiệu của issue.

Issue khác của api-platform/core

Tất cả issue của api-platform/core

Issue tương tự

Thêm issue về PHP

Nhận issue mới trong hộp thư của bạn

Bản tóm tắt ngắn những issue GitHub phù hợp với người mới.