@@ -1238,6 +1238,53 @@ public function testDenormalizeWritableLinks(): void
12381238 $ propertyAccessorProphecy ->setValue ($ actual , 'relatedDummiesWithUnionTypes ' , [0 => $ relatedDummy3 , 1 => $ relatedDummy4 ])->shouldHaveBeenCalled ();
12391239 }
12401240
1241+ public function testUnionTypeDenormalizationFallsThroughAfterTypeConfusionGuardMismatch (): void
1242+ {
1243+ $ data = ['relatedDummyUnion ' => '/related_dummies/1 ' ];
1244+ $ relatedDummy = new RelatedDummy ();
1245+
1246+ $ propertyNameCollectionFactory = $ this ->createStub (PropertyNameCollectionFactoryInterface::class);
1247+ $ propertyNameCollectionFactory ->method ('create ' )->willReturn (new PropertyNameCollection (['relatedDummyUnion ' ]));
1248+
1249+ // Dummy is the mismatching member tried first; RelatedDummy is the one that actually matches.
1250+ $ propertyMetadataFactory = $ this ->createStub (PropertyMetadataFactoryInterface::class);
1251+ $ propertyMetadataFactory ->method ('create ' )->willReturn (
1252+ (new ApiProperty ())
1253+ ->withNativeType (Type::union (Type::object (Dummy::class), Type::object (RelatedDummy::class)))
1254+ ->withWritable (true )->withWritableLink (true )
1255+ );
1256+
1257+ // Always resolves to a RelatedDummy, no matter which type of the union is being attempted.
1258+ $ iriConverter = $ this ->createStub (IriConverterInterface::class);
1259+ $ iriConverter ->method ('getResourceFromIri ' )->willReturn ($ relatedDummy );
1260+
1261+ $ resourceClassResolver = $ this ->createStub (ResourceClassResolverInterface::class);
1262+ $ resourceClassResolver ->method ('isResourceClass ' )->willReturnMap ([
1263+ [Dummy::class, true ],
1264+ [RelatedDummy::class, true ],
1265+ ]);
1266+ $ resourceClassResolver ->method ('getResourceClass ' )->willReturnMap ([
1267+ [null , Dummy::class, Dummy::class],
1268+ [null , RelatedDummy::class, RelatedDummy::class],
1269+ ]);
1270+
1271+ $ propertyAccessor = $ this ->createMock (PropertyAccessorInterface::class);
1272+ $ propertyAccessor ->expects ($ this ->once ())
1273+ ->method ('setValue ' )
1274+ ->with ($ this ->isInstanceOf (Dummy::class), 'relatedDummyUnion ' , $ relatedDummy );
1275+
1276+ $ serializer = $ this ->createStub (SerializerInterface::class);
1277+
1278+ $ normalizer = new class ($ propertyNameCollectionFactory , $ propertyMetadataFactory , $ iriConverter , $ resourceClassResolver , $ propertyAccessor , null , null , [], null , null ) extends AbstractItemNormalizer {};
1279+ $ normalizer ->setSerializer ($ serializer );
1280+
1281+ // The first union member (Dummy) should correctly fail and we expect to fallback to the second
1282+ // member (RelatedDummy).
1283+ $ actual = $ normalizer ->denormalize ($ data , Dummy::class);
1284+
1285+ $ this ->assertInstanceOf (Dummy::class, $ actual );
1286+ }
1287+
12411288 public function testDenormalizeRelationNotFoundReturnsNull (): void
12421289 {
12431290 $ data = [
0 commit comments