2424use ApiPlatform \Metadata \ResourceClassResolverInterface ;
2525use ApiPlatform \Metadata \Util \TypeHelper ;
2626use Symfony \Component \PropertyInfo \PropertyInfoExtractor ;
27+ use Symfony \Component \Serializer \Attribute \DiscriminatorMap ;
2728use Symfony \Component \Serializer \NameConverter \NameConverterInterface ;
2829use Symfony \Component \Serializer \Normalizer \AbstractNormalizer ;
2930use Symfony \Component \TypeInfo \Type \BuiltinType ;
@@ -160,6 +161,8 @@ public function buildSchema(string $className, string $format = 'json', string $
160161 }
161162 }
162163
164+ $ this ->buildDiscriminatorSchema ($ schema , $ definitions , $ definitionName , $ definition , $ inputOrOutputClass , $ format , $ type , $ version , $ options , $ serializerContext , $ isJsonMergePatch );
165+
163166 return $ schema ;
164167 }
165168
@@ -169,16 +172,7 @@ public function buildSchema(string $className, string $format = 'json', string $
169172 private function buildLegacyPropertySchema (Schema $ schema , string $ definitionName , string $ normalizedPropertyName , ApiProperty $ propertyMetadata , array $ serializerContext , string $ format , string $ parentType ): void
170173 {
171174 $ version = $ schema ->getVersion ();
172- if (Schema::VERSION_SWAGGER === $ version || Schema::VERSION_OPENAPI === $ version ) {
173- $ additionalPropertySchema = $ propertyMetadata ->getOpenapiContext ();
174- } else {
175- $ additionalPropertySchema = $ propertyMetadata ->getJsonSchemaContext ();
176- }
177-
178- $ propertySchema = array_merge (
179- $ propertyMetadata ->getSchema () ?? [],
180- $ additionalPropertySchema ?? []
181- );
175+ $ propertySchema = $ this ->getBasePropertySchema ($ propertyMetadata , $ version );
182176
183177 // @see https://github.com/api-platform/core/issues/6299
184178 if (Schema::UNKNOWN_TYPE === ($ propertySchema ['type ' ] ?? null ) && isset ($ propertySchema ['$ref ' ])) {
@@ -300,16 +294,7 @@ private function buildLegacyPropertySchema(Schema $schema, string $definitionNam
300294 private function buildPropertySchema (Schema $ schema , string $ definitionName , string $ normalizedPropertyName , ApiProperty $ propertyMetadata , array $ serializerContext , string $ format , string $ parentType ): void
301295 {
302296 $ version = $ schema ->getVersion ();
303- if (Schema::VERSION_SWAGGER === $ version || Schema::VERSION_OPENAPI === $ version ) {
304- $ additionalPropertySchema = $ propertyMetadata ->getOpenapiContext ();
305- } else {
306- $ additionalPropertySchema = $ propertyMetadata ->getJsonSchemaContext ();
307- }
308-
309- $ propertySchema = array_merge (
310- $ propertyMetadata ->getSchema () ?? [],
311- $ additionalPropertySchema ?? []
312- );
297+ $ propertySchema = $ this ->getBasePropertySchema ($ propertyMetadata , $ version );
313298
314299 $ extraProperties = $ propertyMetadata ->getExtraProperties ();
315300 // see AttributePropertyMetadataFactory
@@ -566,4 +551,124 @@ private function getSchemaValue(array $schema, string $key): array|string|null
566551
567552 return $ schema [$ key ] ?? $ schema ['allOf ' ][0 ][$ key ] ?? $ schema ['anyOf ' ][0 ][$ key ] ?? $ schema ['oneOf ' ][0 ][$ key ] ?? null ;
568553 }
554+
555+ private function getBasePropertySchema (ApiProperty $ propertyMetadata , string $ version ): array
556+ {
557+ if (Schema::VERSION_SWAGGER === $ version || Schema::VERSION_OPENAPI === $ version ) {
558+ $ additionalPropertySchema = $ propertyMetadata ->getOpenapiContext ();
559+ } else {
560+ $ additionalPropertySchema = $ propertyMetadata ->getJsonSchemaContext ();
561+ }
562+
563+ return array_merge (
564+ $ propertyMetadata ->getSchema () ?? [],
565+ $ additionalPropertySchema ?? []
566+ );
567+ }
568+
569+ /**
570+ * @return array<string, mixed>|null
571+ */
572+ private function buildSubclassPropertySchema (Schema $ schema , ApiProperty $ propertyMetadata ): ?array
573+ {
574+ $ propertySchema = $ this ->getBasePropertySchema ($ propertyMetadata , $ schema ->getVersion ());
575+
576+ return $ propertySchema ?: null ;
577+ }
578+
579+ /**
580+ * Builds polymorphic schema (oneOf + discriminator) when the class has a Symfony DiscriminatorMap attribute.
581+ */
582+ private function buildDiscriminatorSchema (Schema $ schema , \ArrayObject $ definitions , string $ definitionName , \ArrayObject $ definition , string $ inputOrOutputClass , string $ format , string $ type , string $ version , array $ options , array $ serializerContext , bool $ isJsonMergePatch ): void
583+ {
584+ $ reflectionClass = new \ReflectionClass ($ inputOrOutputClass );
585+
586+ $ discriminatorMapAttributes = $ reflectionClass ->getAttributes (DiscriminatorMap::class);
587+ if (!$ discriminatorMapAttributes ) {
588+ return ;
589+ }
590+
591+ $ discriminatorMapInstance = $ discriminatorMapAttributes [0 ]->newInstance ();
592+
593+ $ discriminatorReflection = new \ReflectionObject ($ discriminatorMapInstance );
594+ $ typeProperty = $ discriminatorReflection ->getProperty ('typeProperty ' );
595+ $ mappingProperty = $ discriminatorReflection ->getProperty ('mapping ' );
596+
597+ $ typeProperty = $ typeProperty ->getValue ($ discriminatorMapInstance );
598+ $ mapping = $ mappingProperty ->getValue ($ discriminatorMapInstance );
599+
600+ if (!$ mapping ) {
601+ return ;
602+ }
603+
604+ $ uriPrefix = $ this ->getSchemaUriPrefix ($ version );
605+ $ oneOf = [];
606+ $ discriminatorMapping = [];
607+
608+ $ parentPropertyNames = [];
609+ foreach ($ this ->propertyNameCollectionFactory ->create ($ inputOrOutputClass , $ options ) as $ propertyName ) {
610+ $ parentPropertyNames [$ propertyName ] = true ;
611+ }
612+
613+ foreach ($ mapping as $ typeValue => $ subClassName ) {
614+ $ subDefinitionName = $ this ->definitionNameFactory ->create ($ subClassName , $ format , $ subClassName , null , $ serializerContext + ['schema_type ' => $ type ]);
615+
616+ if (isset ($ definitions [$ subDefinitionName ])) {
617+ $ oneOf [] = ['$ref ' => $ uriPrefix .$ subDefinitionName ];
618+ $ discriminatorMapping [$ typeValue ] = $ uriPrefix .$ subDefinitionName ;
619+ continue ;
620+ }
621+
622+ /** @var \ArrayObject<string, array<string, mixed>> $subclassProperties */
623+ $ subclassProperties = new \ArrayObject ();
624+
625+ foreach ($ this ->propertyNameCollectionFactory ->create ($ subClassName , $ options ) as $ propertyName ) {
626+ if (isset ($ parentPropertyNames [$ propertyName ])) {
627+ continue ;
628+ }
629+
630+ $ propertyMetadata = $ this ->propertyMetadataFactory ->create ($ subClassName , $ propertyName , $ options );
631+
632+ if (false === $ propertyMetadata ->isReadable () && false === $ propertyMetadata ->isWritable ()) {
633+ continue ;
634+ }
635+
636+ $ normalizedPropertyName = $ this ->nameConverter ? $ this ->nameConverter ->normalize ($ propertyName , $ subClassName , $ format , $ serializerContext ) : $ propertyName ;
637+ if ($ propertyMetadata ->isRequired () && !$ isJsonMergePatch ) {
638+ $ definition ['required ' ][] = $ normalizedPropertyName ;
639+ }
640+
641+ if ($ propertySchema = $ this ->buildSubclassPropertySchema ($ schema , $ propertyMetadata )) {
642+ $ subclassProperties [$ normalizedPropertyName ] = $ propertySchema ;
643+ }
644+ }
645+
646+ $ allOf = [
647+ ['$ref ' => $ uriPrefix .$ definitionName ],
648+ ];
649+
650+ if (\count ($ subclassProperties ) > 0 ) {
651+ $ extra = ['type ' => 'object ' , 'properties ' => $ subclassProperties ->getArrayCopy ()];
652+ if (isset ($ definition ['required ' ]) && \count ($ definition ['required ' ]) > 0 ) {
653+ $ extra ['required ' ] = $ definition ['required ' ];
654+ }
655+ $ allOf [] = $ extra ;
656+ }
657+
658+ $ subDefinition = new \ArrayObject (['allOf ' => new \ArrayObject ($ allOf )]);
659+ $ definitions [$ subDefinitionName ] = $ subDefinition ;
660+
661+ $ oneOf [] = ['$ref ' => $ uriPrefix .$ subDefinitionName ];
662+ $ discriminatorMapping [$ typeValue ] = $ uriPrefix .$ subDefinitionName ;
663+ }
664+
665+ if (\count ($ oneOf ) > 0 ) {
666+ $ definition ['oneOf ' ] = $ oneOf ;
667+ $ normalizedTypeProperty = $ this ->nameConverter ? $ this ->nameConverter ->normalize ($ typeProperty , $ inputOrOutputClass , $ format , $ serializerContext ) : $ typeProperty ;
668+ $ definition ['discriminator ' ] = [
669+ 'propertyName ' => $ normalizedTypeProperty ,
670+ 'mapping ' => $ discriminatorMapping ,
671+ ];
672+ }
673+ }
569674}
0 commit comments