test: adapt json-streamer and object-mapper fixtures to Symfony 8.1.4 - #8461
Merged
soyuka merged 1 commit intoAug 16, 2026
Merged
Conversation
json-streamer 8.1.4 encodes with JSON_PRESERVE_ZERO_FRACTION, so the float $rating property is now emitted as 5.0 rather than 5; assert a float. object-mapper 8.1.4 honours the target property's #[Map] for the same-name copy, so the untransformed string id reached a ?int setter. Declare the inverse transform the fixture was missing; verified on both 8.1.1 and 8.1.4.
soyuka
force-pushed
the
test/symfony-8-1-4-streamer-mapper-drift
branch
from
August 16, 2026 15:25
ab90147 to
0b0684a
Compare
nicolas-grekas
added a commit
to symfony/symfony
that referenced
this pull request
Aug 17, 2026
…ap] for the same-name copy (soyuka) This PR was merged into the 7.4 branch. Discussion ---------- [ObjectMapper] Only honor an explicitly inbound target #[Map] for the same-name copy | Q | A | ------------- | --- | Branch? | 7.4 | Bug fix? | yes | New feature? | no | Deprecations? | no | Issues | no upstream issue, reported downstream as api-platform/core#8461, details below | License | MIT Follow-up to #65154 ("[ObjectMapper] Fix target property mappings dropped when the source carries metadata"), released in 7.4.16 and 8.1.4. That change was correct in intent but its filter cannot tell an inbound mapping from an outbound one, so a `#[Map(transform:)]` written for one direction now fires in both. ## The problem `ObjectMapper` encodes mapping direction by which side the metadata is read from. `doMap()` resolves `$mapping->source` only when reading from the target, and `$mapping->target` only when reading from the source: ```php $sourcePropertyName = $readMetadataFromTarget ? $mapping->source ?? $propertyName : $propertyName; $targetPropertyName = $mapping->target ?? $propertyName; ``` The same-name cross-read added in #65154 discards that distinction. Its filter defaults both sides to the property name: ```php if (null === $mapping->if && ($mapping->target ?? $propertyName) === $propertyName && ($mapping->source ?? $propertyName) === $propertyName ) { ``` so these three become indistinguishable: | declaration | intent | matched before | | --- | --- | --- | | `#[Map(transform: T)]` | outbound (its own class is the source) | yes | | `#[Map(target: 'id', transform: T)]` | outbound | yes | | `#[Map(source: 'id', transform: T)]` | inbound | yes | Only the third describes an inbound copy. ## Before / after ```php #[Map(target: ProductInput::class)] class Product { #[Map(transform: 'strtoupper')] public string $reference = 'sf-1'; } #[Map(target: Product::class)] class ProductInput { public string $reference = 'sf-1'; } $product = (new ObjectMapper())->map(new ProductInput(), Product::class); ``` - **before:** `$product->reference === 'SF-1'`. The transform declared for `Product` → `ProductInput` also ran on the way in - **after:** `$product->reference === 'sf-1'` Mapping the other way is unchanged and still applies the transform: ```php $input = (new ObjectMapper())->map(new Product(), ProductInput::class); // 'SF-1', before and after ``` ## The fix Require the target-side mapping to name its source explicitly before honoring it in the cross-read: ```diff if (null === $mapping->if + && $propertyName === $mapping->source && ($mapping->target ?? $propertyName) === $propertyName - && ($mapping->source ?? $propertyName) === $propertyName ) { ``` #65154's own fixture declares `#[Map(source: 'type', transform: [ToTypeDto::class, 'transform'])]` on `LeadDto`, so it still matches and `testSameNameTargetPropertyMappingIsHonoredWhenSourceCarriesMetadata` passes unmodified. No existing test or fixture is touched. This also lines the runtime back up with the documented model. The docs teach a bare `transform` on the class that is the *source*: ```php #[Map(target: Product::class)] class ProductInput { #[Map(transform: 'intval')] public string $stockLevel = '100'; } ``` and state that target-side `#[Map(source: ...)]` applies when the source class does not map the property. ## How it was found Reported downstream: api-platform/core#8461. API Platform maps DTOs to Doctrine entities, and an entity carrying `#[Map(transform: 'strval')]` on `$id` for the entity → DTO direction started transforming on writes too, pushing a string into a `?int` setter: ``` Expected argument of type "?int", "string" given at property path "id" ``` Bisected to #65154: pinning `symfony/object-mapper` to 8.1.1 passes, 8.1.4 fails. ## Note for the merge-up: the blast radius differs between 7.4 and 8.x Worth flagging for whoever merges this up, because the 7.4 test coverage does **not** demonstrate the 8.x exposure. On 7.4, `$readMetadataFromTarget === false` is only reached when the source class itself carries a `#[Map]` attribute. The affected population is small. On 8.x, FrameworkBundle registers `ReverseClassObjectMapperMetadataFactory`, which puts source classes into a class map wholesale. That drives many more mappings down the `$readMetadataFromTarget === false` path, so a bare target-side `transform` fires inbound for a much wider set of applications. That is why this surfaced in an API Platform app on 8.x rather than in a plain 7.4 one. I could not run the FrameworkBundle functional tests locally (they need a full root install). I checked its ObjectMapper fixtures by hand. Every `#[Map(transform:)]` there is declared on a source-side class, so none should be affected. The 8.x FrameworkBundle suite is still the real gate, and deserves attention on the merge-up rather than being assumed green from this branch. ## Backward compatibility The behaviour being removed shipped in 7.4.16 (2026-08-05) and 8.1.4 (2026-08-06), so it has been out for roughly ten days. Someone could in principle have started relying on a bare target-side `transform` firing inbound, but: - it is undocumented, and contradicts the documented meaning of a bare `transform` - anyone who wants an inbound transform has `#[Map(source: '<name>', transform: ...)]`, which is unaffected and is the shape #65154's own fixture uses 7.3 does not have the cross-read at all, so 7.4 is the lowest branch where this applies. Commits ------- 1040321 [ObjectMapper] Only honor an explicitly inbound target #[Map] for the same-name copy
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
One of three PRs finishing the 4.3 CI unblock started in #8458. Test-only — no shipped code changes. Both hunks are adaptations to Symfony 8.1.4 components that moved while 4.3 CI was dark.
json-streamer 8.1.4 —
ratingUpstream commit
57ff6c1"[JsonStreamer] Use readable JSON encode defaults" (2026-08-01) addedJSON_PRESERVE_ZERO_FRACTION. Floats with integral values now encode as5.0rather than5.JsonStreamResource::$ratingis declaredfloat(tests/Fixtures/TestBundle/Entity/JsonStreamResource.php:50), so emitting5.0is correct JSON semantics and round-trips the declared type. The previousassertIsIntonly passed because the encoder was lossy about it. Assertions changed toassertIsFloat/assertSame(0.0, ...).The same stale assertion in
testJsonStreamerWriteJsonis fixed too. That test is currently skipped, so it was not failing — it would have failed the day the skip lifts.Note
views(declaredint) is unaffected and still asserted as an int, andtotalItemsis deliberately not touched here — that one was a genuine bug in shipped code, fixed separately.object-mapper 8.1.4 —
MappedResourceWithRelation::$idUpstream commit
38f9183"[ObjectMapper] Honor the target property#[Map]for the same-name copy when the source carries metadata" (2026-08-05) changed the same-name-copy path to consult the target's#[Map].MappedResourceWithRelation::$id(?string) carried no#[Map], so on write it took that path, picked up the entity's#[Map(transform: 'strval')](MappedResourceWithRelationEntity.php:25), and fed a string intosetId(?int)(:43):The fixture only ever declared the forward transform (entity
int→ DTOstring) and left the write direction as an untyped same-name copy that worked only because the mapper was not consulting anything. Declaring the inverse#[Map(transform: 'intval')]is the missing half, not a workaround.Confirmed by pinning:
object-mapper8.1.1 passes, 8.1.4 fails, and the fix passes on both — so it is version-robust rather than tuned to the current release.