Skip to content

Fix ItemNormalizer priority conflict intercepting DateTimeImmutable objects#7737

Draft
Copilot wants to merge 5 commits intomainfrom
copilot/fix-date-time-normalization-issue
Draft

Fix ItemNormalizer priority conflict intercepting DateTimeImmutable objects#7737
Copilot wants to merge 5 commits intomainfrom
copilot/fix-date-time-normalization-issue

Conversation

Copy link

Copilot AI commented Feb 6, 2026

Fix ItemNormalizer intercepting DateTimeImmutable before DateTimeNormalizer

Summary:

All changes complete and verified

This PR resolves a critical priority conflict in the Serializer chain where ItemNormalizer variants were intercepting DateTimeImmutable objects before Symfony's DateTimeNormalizer could handle them, causing a LogicException: Can't get a way to read the property "id" in class "DateTimeImmutable".

Changes Completed:

  • Analyze the issue - found priority conflict in both Symfony and Laravel configurations
  • Fix Symfony configuration - changed ItemNormalizer priority from -895 to -920
  • Fix Laravel configuration - changed ItemNormalizer priority from -895 to -920
  • Fix JsonLd ItemNormalizer priority from -890 to -920 in both Symfony and Laravel
  • Fix other ItemNormalizer variants (HAL, JsonApi, GraphQL) to have priority -920
  • Create comprehensive test coverage validating the fix
  • Address code review feedback - consistent priority across all configs
  • Pass code review - no issues found
  • Pass security scan - no vulnerabilities detected
  • Address PR review comments - simplified provider and improved date validation

Files Changed (8 total):

Configuration (6 files):

  • src/Symfony/Bundle/Resources/config/api.php - ItemNormalizer: -895 → -920
  • src/Symfony/Bundle/Resources/config/jsonld.php - JsonLdItemNormalizer: -890 → -920
  • src/Symfony/Bundle/Resources/config/hal.php - HalItemNormalizer: -890 → -920
  • src/Symfony/Bundle/Resources/config/jsonapi.php - JsonApiItemNormalizer: -890 → -920
  • src/Symfony/Bundle/Resources/config/graphql.php - GraphQlItemNormalizer: -890 → -920
  • src/Laravel/ApiPlatformProvider.php - All ItemNormalizer variants: -890/-895 → -920

Tests (2 files):

  • tests/Fixtures/TestBundle/ApiResource/DateTimeNormalizationIssue.php - Test resource with static provider method
  • tests/Functional/DateTimeNormalizerPriorityTest.php - Functional test with proper date validation

Normalizer Priority Order (Higher values execute first):

-910: DateTimeNormalizer, BackedEnumNormalizer
-915: DateTimeZoneNormalizer, DateIntervalNormalizer
-920: All ItemNormalizer variants ← Fixed to run AFTER specialized normalizers
-1000: ObjectNormalizer

Why -920 specifically?

Using -920 (instead of -915) ensures:

  1. ItemNormalizers run AFTER all DateTime-related normalizers
  2. No undefined execution order from same-priority normalizers
  3. Room for future specialized normalizers between -915 and -920 if needed

Backward Compatibility:

✅ This change is fully backward compatible. It only affects the internal order in which normalizers are checked. The behavior change is a bug fix - DateTimeImmutable objects will now correctly normalize to ISO 8601 strings instead of throwing exceptions.

Original prompt

This section details on the original issue you should resolve

<issue_title>Bug: ItemNormalizer intercepting DateTimeImmutable before DateTimeNormalizer due to priority conflict</issue_title>
<issue_description>API Platform version(s) affected: 4.2.11

Description There is a priority conflict within the Serializer chain where ApiPlatform\Serializer\ItemNormalizer (and its JSON-LD counterpart) intercepts DateTimeImmutable objects before Symfony's native DateTimeNormalizer can handle them.

When a custom normalizer or a specific serialization flow delegates the normalization of a property containing a DateTimeImmutable to the main Serializer, the ItemNormalizer (positioned higher in the priority list) attempts to treat the date as an API resource. It fails while trying to find an identifier (e.g., id), throwing a LogicException.

How to reproduce 1. Define a custom normalizer that delegates work to the global Serializer:

public function normalize(mixed $data, ?string $format = null, array $context = []): array
{
    $normalize = [];
    if ($data->updatedAt) {
        // updatedAt is a DateTimeImmutable
        // This call will trigger the ItemNormalizer instead of DateTimeNormalizer
        $normalize['updatedAt'] = $this->serializer->normalize($data->updatedAt, $format, $context);
    }

    // ... base normalization logic ...
    
    return $normalize;
}
Observe the normalizer order in the compiled container :
        ...

        ApiPlatform\JsonLd\Serializer\ItemNormalizer (Position 18)

        ApiPlatform\Serializer\ItemNormalizer (Position 19)

        Symfony\Component\Serializer\Normalizer\DateTimeNormalizer (Position 20)

        ...

The execution fails with the following error: LogicException: Can't get a way to read the property "id" in class "DateTimeImmutable".

Possible Solution

The DateTimeNormalizer (and other specialized Symfony normalizers like UidNormalizer) should have a higher priority than ItemNormalizer. Currently, ItemNormalizer acts as a "greedy" catch-all for any object that is not explicitly handled by a previous specialized normalizer.

Lowering the priority of ItemNormalizer would ensure dates are formatted before the system tries to resolve them as API resources.

...

ApiPlatform\JsonLd\Serializer\ItemNormalizer (Position 18)

ApiPlatform\Serializer\ItemNormalizer (Position 19)

Symfony\Component\Serializer\Normalizer\DateTimeNormalizer (Position 20 - Too low!)

...

Symfony\Component\Serializer\Normalizer\ObjectNormalizer (Position 33)

Because the custom normalizer calls the global Serializer for a date, the Serializer starts searching from the top of the list and matches the ItemNormalizer at Position 18/19 because it is a class, before reaching the correct specialist at Position 20.</issue_description>

Comments on the Issue (you are @copilot in this section)


💬 We'd love your input! Share your thoughts on Copilot coding agent in our 2 minute survey.

Copilot AI and others added 3 commits February 6, 2026 15:57
Changed all ItemNormalizer variants priority from -890/-895 to -915/-920 to ensure they run AFTER Symfony's DateTimeNormalizer (-910). This prevents ItemNormalizer from incorrectly attempting to normalize DateTimeImmutable objects as API resources.

Co-authored-by: soyuka <1321971+soyuka@users.noreply.github.com>
Created functional test to verify DateTimeImmutable objects are correctly normalized by DateTimeNormalizer instead of being incorrectly intercepted by ItemNormalizer. Tests multiple formats (JSON-LD, JSON, HAL, JSON:API).

Co-authored-by: soyuka <1321971+soyuka@users.noreply.github.com>
Changed all ItemNormalizer variants to use priority -920 (instead of -915) to ensure they run AFTER all DateTime-related normalizers (-910, -915). This avoids potential priority conflicts where same-priority normalizers have undefined execution order. Added explanatory comment in Laravel config.

Co-authored-by: soyuka <1321971+soyuka@users.noreply.github.com>
Copilot AI changed the title [WIP] Fix ItemNormalizer priority conflict with DateTimeImmutable Fix ItemNormalizer priority conflict intercepting DateTimeImmutable objects Feb 6, 2026
Copilot AI requested a review from soyuka February 6, 2026 16:04
- Move provider logic to static method inside DateTimeNormalizationIssue fixture
- Remove separate DateTimeNormalizationIssueProvider class
- Use short class name for provider reference with proper import
- Replace regex validation with DateTime::createFromFormat for better date testing

Co-authored-by: soyuka <1321971+soyuka@users.noreply.github.com>
Copilot AI requested a review from soyuka February 6, 2026 19:29
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Bug: ItemNormalizer intercepting DateTimeImmutable before DateTimeNormalizer due to priority conflict

2 participants