@@ -124,6 +124,11 @@ final class SchemaFactory implements SchemaFactoryInterface, SchemaFactoryAwareI
124124 ],
125125 ];
126126
127+ /**
128+ * @var array<string, bool>
129+ */
130+ private $ builtSchema = [];
131+
127132 public function __construct (private readonly SchemaFactoryInterface $ schemaFactory , private readonly PropertyMetadataFactoryInterface $ propertyMetadataFactory , ResourceClassResolverInterface $ resourceClassResolver , ?ResourceMetadataCollectionFactoryInterface $ resourceMetadataFactory = null , private ?DefinitionNameFactoryInterface $ definitionNameFactory = null )
128133 {
129134 if (!$ definitionNameFactory ) {
@@ -163,12 +168,12 @@ public function buildSchema(string $className, string $format = 'jsonapi', strin
163168 // We don't use the serializer context here as JSON:API doesn't leverage serializer groups for related resources.
164169 // That is done by query parameter. @see https://jsonapi.org/format/#fetching-includes
165170 $ jsonApiSerializerContext = $ serializerContext ;
166- if (false === ($ serializerContext [self ::DISABLE_JSON_SCHEMA_SERIALIZER_GROUPS ] ?? true )) {
171+ if (true === ($ serializerContext [self ::DISABLE_JSON_SCHEMA_SERIALIZER_GROUPS ] ?? true ) && $ inputOrOutputClass === $ className ) {
167172 unset($ jsonApiSerializerContext ['groups ' ]);
168173 }
169174
170175 $ schema = $ this ->schemaFactory ->buildSchema ($ className , 'json ' , $ type , $ operation , $ schema , $ jsonApiSerializerContext , $ forceCollection );
171- $ definitionName = $ this ->definitionNameFactory ->create ($ className , $ format , $ className , $ operation , $ serializerContext );
176+ $ definitionName = $ this ->definitionNameFactory ->create ($ inputOrOutputClass , $ format , $ className , $ operation , $ jsonApiSerializerContext );
172177 $ prefix = $ this ->getSchemaUriPrefix ($ schema ->getVersion ());
173178 $ definitions = $ schema ->getDefinitions ();
174179 $ collectionKey = $ schema ->getItemsDefinitionKey ();
@@ -183,11 +188,6 @@ public function buildSchema(string $className, string $format = 'jsonapi', strin
183188 $ key = $ schema ->getRootDefinitionKey () ?? $ collectionKey ;
184189 $ properties = $ definitions [$ definitionName ]['properties ' ] ?? [];
185190
186- // Prevent reapplying
187- if (isset ($ definitions [$ key ]['description ' ])) {
188- $ definitions [$ definitionName ]['description ' ] = $ definitions [$ key ]['description ' ];
189- }
190-
191191 if (Error::class === $ className && !isset ($ properties ['errors ' ])) {
192192 $ definitions [$ definitionName ]['properties ' ] = [
193193 'errors ' => [
@@ -213,41 +213,41 @@ public function buildSchema(string $className, string $format = 'jsonapi', strin
213213 return $ schema ;
214214 }
215215
216- if (($ schema ['type ' ] ?? '' ) === 'array ' ) {
217- if (!isset ($ definitions [self ::COLLECTION_BASE_SCHEMA_NAME ])) {
218- $ definitions [self ::COLLECTION_BASE_SCHEMA_NAME ] = [
219- 'type ' => 'object ' ,
220- 'properties ' => [
221- 'links ' => self ::LINKS_PROPS ,
222- 'meta ' => self ::META_PROPS ,
223- 'data ' => [
224- 'type ' => 'array ' ,
225- ],
226- ],
227- 'required ' => ['data ' ],
228- ];
229- }
230-
231- unset($ schema ['items ' ]);
232- unset($ schema ['type ' ]);
233-
234- $ properties = $ this ->buildDefinitionPropertiesSchema ($ key , $ className , $ format , $ type , $ operation , $ schema , []);
235- $ properties ['data ' ]['properties ' ]['attributes ' ]['$ref ' ] = $ prefix .$ key ;
216+ if (($ schema ['type ' ] ?? '' ) !== 'array ' ) {
217+ return $ schema ;
218+ }
236219
237- $ schema ['description ' ] = "$ definitionName collection. " ;
238- $ schema ['allOf ' ] = [
239- ['$ref ' => $ prefix .self ::COLLECTION_BASE_SCHEMA_NAME ],
240- ['type ' => 'object ' , 'properties ' => [
220+ if (!isset ($ definitions [self ::COLLECTION_BASE_SCHEMA_NAME ])) {
221+ $ definitions [self ::COLLECTION_BASE_SCHEMA_NAME ] = [
222+ 'type ' => 'object ' ,
223+ 'properties ' => [
224+ 'links ' => self ::LINKS_PROPS ,
225+ 'meta ' => self ::META_PROPS ,
241226 'data ' => [
242227 'type ' => 'array ' ,
243- 'items ' => $ properties ['data ' ],
244228 ],
245- ]],
229+ ],
230+ 'required ' => ['data ' ],
246231 ];
247-
248- return $ schema ;
249232 }
250233
234+ unset($ schema ['items ' ]);
235+ unset($ schema ['type ' ]);
236+
237+ $ properties = $ this ->buildDefinitionPropertiesSchema ($ key , $ className , $ format , $ type , $ operation , $ schema , []);
238+ $ properties ['data ' ]['properties ' ]['attributes ' ]['$ref ' ] = $ prefix .$ key ;
239+
240+ $ schema ['description ' ] = "$ definitionName collection. " ;
241+ $ schema ['allOf ' ] = [
242+ ['$ref ' => $ prefix .self ::COLLECTION_BASE_SCHEMA_NAME ],
243+ ['type ' => 'object ' , 'properties ' => [
244+ 'data ' => [
245+ 'type ' => 'array ' ,
246+ 'items ' => $ properties ['data ' ],
247+ ],
248+ ]],
249+ ];
250+
251251 return $ schema ;
252252 }
253253
@@ -279,8 +279,23 @@ private function buildDefinitionPropertiesSchema(string $key, string $className,
279279 $ inputOrOutputClass = $ this ->findOutputClass ($ relatedClassName , $ type , $ operation , $ serializerContext );
280280 $ serializerContext ??= $ this ->getSerializerContext ($ operation , $ type );
281281 $ definitionName = $ this ->definitionNameFactory ->create ($ relatedClassName , $ format , $ inputOrOutputClass , $ operation , $ serializerContext );
282- $ ref = $ this ->getSchemaUriPrefix ($ schema ->getVersion ()).$ definitionName ;
283- $ refs [$ ref ] = '$ref ' ;
282+
283+ // to avoid recursion
284+ if ($ this ->builtSchema [$ definitionName ] ?? false ) {
285+ $ refs [$ this ->getSchemaUriPrefix ($ schema ->getVersion ()).$ definitionName ] = '$ref ' ;
286+ continue ;
287+ }
288+
289+ if (!isset ($ definitions [$ definitionName ])) {
290+ $ this ->builtSchema [$ definitionName ] = true ;
291+ $ subSchema = new Schema ($ schema ->getVersion ());
292+ $ subSchema ->setDefinitions ($ schema ->getDefinitions ());
293+ $ subSchema = $ this ->buildSchema ($ relatedClassName , $ format , $ type , $ operation , $ subSchema , $ serializerContext + [self ::FORCE_SUBSCHEMA => true ], false );
294+ $ schema ->setDefinitions ($ subSchema ->getDefinitions ());
295+ $ definitions = $ schema ->getDefinitions ();
296+ }
297+
298+ $ refs [$ this ->getSchemaUriPrefix ($ schema ->getVersion ()).$ definitionName ] = '$ref ' ;
284299 }
285300 $ relatedDefinitions [$ propertyName ] = array_flip ($ refs );
286301 if ($ isOne ) {
@@ -317,7 +332,7 @@ private function buildDefinitionPropertiesSchema(string $key, string $className,
317332 'description ' => 'Related resources requested via the "include" query parameter. ' ,
318333 'type ' => 'array ' ,
319334 'items ' => [
320- 'anyOf ' => array_values ($ relatedDefinitions ),
335+ 'anyOf ' => array_unique ( array_values ($ relatedDefinitions) ),
321336 ],
322337 'readOnly ' => true ,
323338 'externalDocs ' => [
@@ -327,24 +342,6 @@ private function buildDefinitionPropertiesSchema(string $key, string $className,
327342 ];
328343 }
329344
330- if ($ required = $ definitions [$ key ]['required ' ] ?? null ) {
331- foreach ($ required as $ i => $ require ) {
332- if (isset ($ relationships [$ require ])) {
333- $ replacement ['relationships ' ]['required ' ][] = $ require ;
334- unset($ required [$ i ]);
335- }
336- }
337-
338- $ replacement ['attributes ' ] = [
339- 'allOf ' => [
340- $ replacement ['attributes ' ],
341- ['type ' => 'object ' , 'required ' => $ required ],
342- ],
343- ];
344-
345- unset($ definitions [$ key ]['required ' ]);
346- }
347-
348345 return [
349346 'data ' => [
350347 'type ' => 'object ' ,
0 commit comments