Skip to content

fix(jsonschema): generation of non-LD+JSON distinct schema formats #6236

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 4 commits into from
Mar 25, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
23 changes: 16 additions & 7 deletions src/Hal/JsonSchema/SchemaFactory.php
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,7 @@
* @author Kévin Dunglas <[email protected]>
* @author Jachim Coudenys <[email protected]>
*/
final class SchemaFactory implements SchemaFactoryInterface
final class SchemaFactory implements SchemaFactoryInterface, SchemaFactoryAwareInterface
{
private const HREF_PROP = [
'href' => [
Expand All @@ -46,7 +46,6 @@ final class SchemaFactory implements SchemaFactoryInterface

public function __construct(private readonly SchemaFactoryInterface $schemaFactory)
{
$this->addDistinctFormat('jsonhal');
if ($this->schemaFactory instanceof SchemaFactoryAwareInterface) {
$this->schemaFactory->setSchemaFactory($this);
}
Expand Down Expand Up @@ -79,8 +78,18 @@ public function buildSchema(string $className, string $format = 'jsonhal', strin
$schema['type'] = 'object';
$schema['properties'] = [
'_embedded' => [
'type' => 'array',
'items' => $items,
'anyOf' => [
[
'type' => 'object',
'properties' => [
'item' => [
'type' => 'array',
'items' => $items,
],
],
],
['type' => 'object'],
],
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

something's bothering me here I need to check that, I don't understand why we put array in the first place.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Introduced in #4357, as was addDistinctFormat()

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

https://datatracker.ietf.org/doc/html/draft-kelly-json-hal#section-4.1.2

It is an object whose property names are link relation types (as defined by [RFC5988]) and values are either a Resource Object or an array of Resource Objects.

can you revert this change? I think that our HAL representation always uses an array and we use items as key.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Doesn't that make the behaviour in ApiPlatform\Hal\Serializer\CollectionNormalizer incorrect?

protected function getItemsData(iterable $object, ?string $format = null, array $context = []): array
{
$data = [];
foreach ($object as $obj) {
$item = $this->normalizer->normalize($obj, $format, $context);
if (!\is_array($item)) {
throw new UnexpectedValueException('Expected item to be an array');
}
$data['_embedded']['item'][] = $item;
$data['_links']['item'][] = $item['_links']['self'] ?? null;
}
return $data;
}

The changes in ApiTestCaseTest (testing jsonld and jsonhal as formats) trigger a failure condition without this change:

1) ApiPlatform\Tests\Symfony\Bundle\Test\ApiTestCaseTest::testAssertMatchesResourceCollectionJsonSchema with data set "jsonhal" ('jsonhal', 'application/hal+json')
Failed asserting that Array &0 (
    '_links' => Array &1 (
        'self' => Array &2 (
            'href' => '/resource_interfaces'
        )
        'item' => Array &3 (
            0 => Array &4 (
                'href' => '/resource_interfaces/item1'
            )
            1 => Array &5 (
                'href' => '/resource_interfaces/item2'
            )
        )
    )
    '_embedded' => Array &6 (
        'item' => Array &7 (
            0 => Array &8 (
                '_links' => Array &9 (
                    'self' => Array &10 (
                        'href' => '/resource_interfaces/item1'
                    )
                )
                'foo' => 'item1'
                'fooz' => 'fooz'
            )
            1 => Array &11 (
                '_links' => Array &12 (
                    'self' => Array &13 (
                        'href' => '/resource_interfaces/item2'
                    )
                )
                'foo' => 'item2'
                'fooz' => 'fooz'
            )
        )
    )
) matches the provided JSON Schema.
_embedded: Object value found, but an array is required

2) ApiPlatform\Tests\Symfony\Bundle\Test\ApiTestCaseTest::testAssertMatchesResourceCollectionJsonSchemaKeepSerializationContext with data set "jsonhal" ('jsonhal', 'application/hal+json')
Failed asserting that Array &0 (
    '_links' => Array &1 (
        'self' => Array &2 (
            'href' => '/issue-6146-parents'
        )
        'item' => Array &3 (
            0 => Array &4 (
                'href' => '/issue-6146-parents/1'
            )
        )
    )
    'totalItems' => 1
    'itemsPerPage' => 3
    '_embedded' => Array &5 (
        'item' => Array &6 (
            0 => Array &7 (
                '_links' => Array &8 (
                    'self' => Array &9 (
                        'href' => '/issue-6146-parents/1'
                    )
                )
            )
        )
    )
) matches the provided JSON Schema.
_embedded: Object value found, but an array is required

3) ApiPlatform\Tests\Symfony\Bundle\Test\ApiTestCaseTest::testAssertMatchesResourceItemAndCollectionJsonSchemaOutputWithContext with data set "jsonhal" ('jsonhal', 'application/hal+json')
Failed asserting that Array &0 (
    '_links' => Array &1 (
        'self' => Array &2 (
            'href' => '/users-with-groups'
        )
        'item' => Array &3 (
            0 => Array &4 (
                'href' => '/users/1'
            )
        )
    )
    'totalItems' => 1
    'itemsPerPage' => 3
    '_embedded' => Array &5 (
        'item' => Array &6 (
            0 => Array &7 (
                '_links' => Array &8 (
                    'self' => Array &9 (
                        'href' => '/users/1'
                    )
                )
                'fullname' => 'Grégoire'
            )
        )
    )
) matches the provided JSON Schema.
_embedded: Object value found, but an array is required

Copy link
Contributor Author

@GwendolenLynch GwendolenLynch Mar 25, 2024

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

From https://datatracker.ietf.org/doc/html/draft-kelly-json-hal#section-6

{
  "_links": {
    "self": { "href": "/orders" },
    "next": { "href": "/orders?page=2" },
    "find": { "href": "/orders{?id}", "templated": true }
  },
  "_embedded": {
    "orders": [{
        "_links": {
          "self": { "href": "/orders/123" },
          "basket": { "href": "/baskets/98712" },
          "customer": { "href": "/customers/7809" }
        },
        "total": 30.00,
        "currency": "USD",
        "status": "shipped",
      },{
        "_links": {
          "self": { "href": "/orders/124" },
          "basket": { "href": "/baskets/97213" },
          "customer": { "href": "/customers/12369" }
        },
        "total": 20.00,
        "currency": "USD",
        "status": "processing"
    }]
  },
  "currentlyProcessing": 14,
  "shippedToday": 20
}

Which also comes back to a previous question as to the key(s) of _embedded … internally items is used but the examples in the draft seem to use the resource name.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

To be correct the schema should be anyOf object or array. We force item at

$data['_embedded']['item'][] = $item;
therefore we need the key at item as I see it.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

As in how I did it originally in 97f7dff?

Copy link
Contributor Author

@GwendolenLynch GwendolenLynch Mar 25, 2024

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Ugh, never mind, it clicked what you meant … now (I think) I get you 😇

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Looking too hard at the same thing for too long and got my brain in a twist.

Copy link
Member

@soyuka soyuka Mar 25, 2024

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Right now we may have (as this is what we use for our collections and):

'anyOf' => [
    [
        // see features/hal/collection.feature
        'type' => 'object',
        'properties' => [
            'item' => [
                'type' => 'array',
                'items' => $items,
            ],
        ],
    ],
    // see features/hal/hal.feature
    [ 'type' => 'object' ]
],

Although the spec says that it can also be

// according to what I understand from the spec this is also valid:
  [
        'type' => 'array'
  ],

I'm okay with adding it if you want

],
'totalItems' => [
'type' => 'integer',
Expand Down Expand Up @@ -127,10 +136,10 @@ public function buildSchema(string $className, string $format = 'jsonhal', strin
return $schema;
}

public function addDistinctFormat(string $format): void
public function setSchemaFactory(SchemaFactoryInterface $schemaFactory): void
{
if (method_exists($this->schemaFactory, 'addDistinctFormat')) {
$this->schemaFactory->addDistinctFormat($format);
if ($this->schemaFactory instanceof SchemaFactoryAwareInterface) {
$this->schemaFactory->setSchemaFactory($schemaFactory);
}
}
}
10 changes: 4 additions & 6 deletions src/Hydra/JsonSchema/SchemaFactory.php
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,6 @@

use ApiPlatform\JsonLd\ContextBuilder;
use ApiPlatform\JsonSchema\Schema;
use ApiPlatform\JsonSchema\SchemaFactory as BaseSchemaFactory;
use ApiPlatform\JsonSchema\SchemaFactoryAwareInterface;
use ApiPlatform\JsonSchema\SchemaFactoryInterface;
use ApiPlatform\Metadata\Operation;
Expand All @@ -25,7 +24,7 @@
*
* @author Kévin Dunglas <[email protected]>
*/
final class SchemaFactory implements SchemaFactoryInterface
final class SchemaFactory implements SchemaFactoryInterface, SchemaFactoryAwareInterface
{
private const BASE_PROP = [
'readOnly' => true,
Expand Down Expand Up @@ -60,7 +59,6 @@ final class SchemaFactory implements SchemaFactoryInterface

public function __construct(private readonly SchemaFactoryInterface $schemaFactory)
{
$this->addDistinctFormat('jsonld');
if ($this->schemaFactory instanceof SchemaFactoryAwareInterface) {
$this->schemaFactory->setSchemaFactory($this);
}
Expand Down Expand Up @@ -184,10 +182,10 @@ public function buildSchema(string $className, string $format = 'jsonld', string
return $schema;
}

public function addDistinctFormat(string $format): void
public function setSchemaFactory(SchemaFactoryInterface $schemaFactory): void
{
if ($this->schemaFactory instanceof BaseSchemaFactory) {
$this->schemaFactory->addDistinctFormat($format);
if ($this->schemaFactory instanceof SchemaFactoryAwareInterface) {
$this->schemaFactory->setSchemaFactory($schemaFactory);
}
}
}
15 changes: 2 additions & 13 deletions src/JsonSchema/SchemaFactory.php
Original file line number Diff line number Diff line change
Expand Up @@ -36,14 +36,13 @@
final class SchemaFactory implements SchemaFactoryInterface, SchemaFactoryAwareInterface
{
use ResourceClassInfoTrait;
private array $distinctFormats = [];
private ?TypeFactoryInterface $typeFactory = null;
private ?SchemaFactoryInterface $schemaFactory = null;
// Edge case where the related resource is not readable (for example: NotExposed) but we have groups to read the whole related object
public const FORCE_SUBSCHEMA = '_api_subschema_force_readable_link';
public const OPENAPI_DEFINITION_NAME = 'openapi_definition_name';

public function __construct(?TypeFactoryInterface $typeFactory, ResourceMetadataCollectionFactoryInterface $resourceMetadataFactory, private readonly PropertyNameCollectionFactoryInterface $propertyNameCollectionFactory, private readonly PropertyMetadataFactoryInterface $propertyMetadataFactory, private readonly ?NameConverterInterface $nameConverter = null, ?ResourceClassResolverInterface $resourceClassResolver = null)
public function __construct(?TypeFactoryInterface $typeFactory, ResourceMetadataCollectionFactoryInterface $resourceMetadataFactory, private readonly PropertyNameCollectionFactoryInterface $propertyNameCollectionFactory, private readonly PropertyMetadataFactoryInterface $propertyMetadataFactory, private readonly ?NameConverterInterface $nameConverter = null, ?ResourceClassResolverInterface $resourceClassResolver = null, private readonly ?array $distinctFormats = null)
{
if ($typeFactory) {
$this->typeFactory = $typeFactory;
Expand All @@ -53,16 +52,6 @@ public function __construct(?TypeFactoryInterface $typeFactory, ResourceMetadata
$this->resourceClassResolver = $resourceClassResolver;
}

/**
* When added to the list, the given format will lead to the creation of a new definition.
*
* @internal
*/
public function addDistinctFormat(string $format): void
{
$this->distinctFormats[$format] = true;
}

/**
* {@inheritdoc}
*/
Expand Down Expand Up @@ -267,7 +256,7 @@ private function buildDefinitionName(string $className, string $format = 'json',
$prefix .= '.'.$shortName;
}

if (isset($this->distinctFormats[$format])) {
if ('json' !== $format && ($this->distinctFormats[$format] ?? false)) {
// JSON is the default, and so isn't included in the definition name
$prefix .= '.'.$format;
}
Expand Down
15 changes: 13 additions & 2 deletions src/Symfony/Bundle/DependencyInjection/ApiPlatformExtension.php
Original file line number Diff line number Diff line change
Expand Up @@ -120,6 +120,16 @@ public function load(array $configs, ContainerBuilder $container): void
$patchFormats = $this->getFormats($config['patch_formats']);
$errorFormats = $this->getFormats($config['error_formats']);
$docsFormats = $this->getFormats($config['docs_formats']);
$jsonSchemaFormats = $config['jsonschema_formats'];

if (!$jsonSchemaFormats) {
foreach (array_keys($formats) as $f) {
// Distinct JSON-based formats must have names that start with 'json'
if (str_starts_with($f, 'json')) {
$jsonSchemaFormats[$f] = true;
}
}
}

if (!isset($errorFormats['json'])) {
$errorFormats['json'] = ['application/problem+json', 'application/json'];
Expand All @@ -144,7 +154,7 @@ public function load(array $configs, ContainerBuilder $container): void
$docsFormats['jsonopenapi'] = ['application/vnd.openapi+json'];
}

$this->registerCommonConfiguration($container, $config, $loader, $formats, $patchFormats, $errorFormats, $docsFormats);
$this->registerCommonConfiguration($container, $config, $loader, $formats, $patchFormats, $errorFormats, $docsFormats, $jsonSchemaFormats);
$this->registerMetadataConfiguration($container, $config, $loader);
$this->registerOAuthConfiguration($container, $config);
$this->registerOpenApiConfiguration($container, $config, $loader);
Expand Down Expand Up @@ -185,7 +195,7 @@ public function load(array $configs, ContainerBuilder $container): void
$this->registerInflectorConfiguration($config);
}

private function registerCommonConfiguration(ContainerBuilder $container, array $config, XmlFileLoader $loader, array $formats, array $patchFormats, array $errorFormats, array $docsFormats): void
private function registerCommonConfiguration(ContainerBuilder $container, array $config, XmlFileLoader $loader, array $formats, array $patchFormats, array $errorFormats, array $docsFormats, array $jsonSchemaFormats): void
{
$loader->load('symfony/events.xml');
$loader->load('symfony/controller.xml');
Expand Down Expand Up @@ -218,6 +228,7 @@ private function registerCommonConfiguration(ContainerBuilder $container, array
$container->setParameter('api_platform.patch_formats', $patchFormats);
$container->setParameter('api_platform.error_formats', $errorFormats);
$container->setParameter('api_platform.docs_formats', $docsFormats);
$container->setParameter('api_platform.jsonschema_formats', $jsonSchemaFormats);
$container->setParameter('api_platform.eager_loading.enabled', $this->isConfigEnabled($container, $config['eager_loading']));
$container->setParameter('api_platform.eager_loading.max_joins', $config['eager_loading']['max_joins']);
$container->setParameter('api_platform.eager_loading.fetch_partial', $config['eager_loading']['fetch_partial']);
Expand Down
8 changes: 8 additions & 0 deletions src/Symfony/Bundle/DependencyInjection/Configuration.php
Original file line number Diff line number Diff line change
Expand Up @@ -174,6 +174,14 @@ public function getConfigTreeBuilder(): TreeBuilder
'jsonproblem' => ['mime_types' => ['application/problem+json']],
'json' => ['mime_types' => ['application/problem+json', 'application/json']],
]);
$rootNode
->children()
->arrayNode('jsonschema_formats')
->scalarPrototype()->end()
->defaultValue([])
->info('The JSON formats to compute the JSON Schemas for.')
->end()
->end();

$this->addDefaultsSection($rootNode);

Expand Down
1 change: 1 addition & 0 deletions src/Symfony/Bundle/Resources/config/json_schema.xml
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@
<argument type="service" id="api_platform.metadata.property.metadata_factory" />
<argument type="service" id="api_platform.name_converter" on-invalid="ignore" />
<argument type="service" id="api_platform.resource_class_resolver" />
<argument on-invalid="ignore">%api_platform.jsonschema_formats%</argument>
</service>
<service id="ApiPlatform\JsonSchema\SchemaFactoryInterface" alias="api_platform.json_schema.schema_factory" />

Expand Down
5 changes: 4 additions & 1 deletion tests/Hal/JsonSchema/SchemaFactoryTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -53,7 +53,10 @@ protected function setUp(): void
null,
$resourceMetadataFactory->reveal(),
$propertyNameCollectionFactory->reveal(),
$propertyMetadataFactory->reveal()
$propertyMetadataFactory->reveal(),
null,
null,
['jsonapi' => true, 'jsonhal' => true, 'jsonld' => true],
);

$hydraSchemaFactory = new HydraSchemaFactory($baseSchemaFactory);
Expand Down
5 changes: 4 additions & 1 deletion tests/Hydra/JsonSchema/SchemaFactoryTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -54,7 +54,10 @@ protected function setUp(): void
null,
$resourceMetadataFactoryCollection->reveal(),
$propertyNameCollectionFactory->reveal(),
$propertyMetadataFactory->reveal()
$propertyMetadataFactory->reveal(),
null,
null,
['jsonapi' => true, 'jsonhal' => true, 'jsonld' => true],
);

$this->schemaFactory = new SchemaFactory($baseSchemaFactory);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -98,6 +98,7 @@ private function runDefaultConfigTests(array $doctrineIntegrationsToLoad = ['orm
'jsonld' => ['mime_types' => ['application/ld+json']],
'json' => ['mime_types' => ['application/problem+json', 'application/json']],
],
'jsonschema_formats' => [],
'exception_to_status' => [
ExceptionInterface::class => Response::HTTP_BAD_REQUEST,
InvalidArgumentException::class => Response::HTTP_BAD_REQUEST,
Expand Down
69 changes: 47 additions & 22 deletions tests/Symfony/Bundle/Test/ApiTestCaseTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,13 @@ class ApiTestCaseTest extends ApiTestCase
{
use ExpectDeprecationTrait;

public static function providerFormats(): iterable
{
// yield 'jsonapi' => ['jsonapi', 'application/vnd.api+json'];
yield 'jsonhal' => ['jsonhal', 'application/hal+json'];
yield 'jsonld' => ['jsonld', 'application/ld+json'];
}

public function testAssertJsonContains(): void
{
self::createClient()->request('GET', '/');
Expand Down Expand Up @@ -122,13 +129,19 @@ public function testAssertMatchesJsonSchema(): void
$this->assertMatchesJsonSchema(json_decode($jsonSchema, true));
}

public function testAssertMatchesResourceCollectionJsonSchema(): void
/**
* @dataProvider providerFormats
*/
public function testAssertMatchesResourceCollectionJsonSchema(string $format, string $mimeType): void
{
self::createClient()->request('GET', '/resource_interfaces');
$this->assertMatchesResourceCollectionJsonSchema(ResourceInterface::class);
self::createClient()->request('GET', '/resource_interfaces', ['headers' => ['Accept' => $mimeType]]);
$this->assertMatchesResourceCollectionJsonSchema(ResourceInterface::class, format: $format);
}

public function testAssertMatchesResourceCollectionJsonSchemaKeepSerializationContext(): void
/**
* @dataProvider providerFormats
*/
public function testAssertMatchesResourceCollectionJsonSchemaKeepSerializationContext(string $format, string $mimeType): void
{
$this->recreateSchema();

Expand All @@ -146,20 +159,26 @@ public function testAssertMatchesResourceCollectionJsonSchemaKeepSerializationCo
$manager->persist($child);
$manager->flush();

self::createClient()->request('GET', "issue-6146-parents/{$parent->getId()}");
$this->assertMatchesResourceItemJsonSchema(Issue6146Parent::class);
self::createClient()->request('GET', "issue-6146-parents/{$parent->getId()}", ['headers' => ['Accept' => $mimeType]]);
$this->assertMatchesResourceItemJsonSchema(Issue6146Parent::class, format: $format);

self::createClient()->request('GET', '/issue-6146-parents');
$this->assertMatchesResourceCollectionJsonSchema(Issue6146Parent::class);
self::createClient()->request('GET', '/issue-6146-parents', ['headers' => ['Accept' => $mimeType]]);
$this->assertMatchesResourceCollectionJsonSchema(Issue6146Parent::class, format: $format);
}

public function testAssertMatchesResourceItemJsonSchema(): void
/**
* @dataProvider providerFormats
*/
public function testAssertMatchesResourceItemJsonSchema(string $format, string $mimeType): void
{
self::createClient()->request('GET', '/resource_interfaces/some-id');
$this->assertMatchesResourceItemJsonSchema(ResourceInterface::class);
self::createClient()->request('GET', '/resource_interfaces/some-id', ['headers' => ['Accept' => $mimeType]]);
$this->assertMatchesResourceItemJsonSchema(ResourceInterface::class, format: $format);
}

public function testAssertMatchesResourceItemJsonSchemaWithCustomJson(): void
/**
* @dataProvider providerFormats
*/
public function testAssertMatchesResourceItemJsonSchemaWithCustomJson(string $format, string $mimeType): void
{
$this->recreateSchema();

Expand All @@ -169,11 +188,14 @@ public function testAssertMatchesResourceItemJsonSchemaWithCustomJson(): void
$manager->persist($jsonSchemaContextDummy);
$manager->flush();

self::createClient()->request('GET', '/json_schema_context_dummies/1');
$this->assertMatchesResourceItemJsonSchema(JsonSchemaContextDummy::class);
self::createClient()->request('GET', '/json_schema_context_dummies/1', ['headers' => ['Accept' => $mimeType]]);
$this->assertMatchesResourceItemJsonSchema(JsonSchemaContextDummy::class, format: $format);
}

public function testAssertMatchesResourceItemJsonSchemaOutput(): void
/**
* @dataProvider providerFormats
*/
public function testAssertMatchesResourceItemJsonSchemaOutput(string $format, string $mimeType): void
{
$this->recreateSchema();

Expand All @@ -184,11 +206,14 @@ public function testAssertMatchesResourceItemJsonSchemaOutput(): void
$dummyDtoInputOutput->num = 54;
$manager->persist($dummyDtoInputOutput);
$manager->flush();
self::createClient()->request('GET', '/dummy_dto_input_outputs/1');
$this->assertMatchesResourceItemJsonSchema(DummyDtoInputOutput::class);
self::createClient()->request('GET', '/dummy_dto_input_outputs/1', ['headers' => ['Accept' => $mimeType]]);
$this->assertMatchesResourceItemJsonSchema(DummyDtoInputOutput::class, format: $format);
}

public function testAssertMatchesResourceItemAndCollectionJsonSchemaOutputWithContext(): void
/**
* @dataProvider providerFormats
*/
public function testAssertMatchesResourceItemAndCollectionJsonSchemaOutputWithContext(string $format, string $mimeType): void
{
$this->recreateSchema();

Expand All @@ -201,11 +226,11 @@ public function testAssertMatchesResourceItemAndCollectionJsonSchemaOutputWithCo
$manager->persist($user);
$manager->flush();

self::createClient()->request('GET', "/users-with-groups/{$user->getId()}");
$this->assertMatchesResourceItemJsonSchema(User::class, null, 'jsonld', ['groups' => ['api-test-case-group']]);
self::createClient()->request('GET', "/users-with-groups/{$user->getId()}", ['headers' => ['Accept' => $mimeType]]);
$this->assertMatchesResourceItemJsonSchema(User::class, null, $format, ['groups' => ['api-test-case-group']]);

self::createClient()->request('GET', '/users-with-groups');
$this->assertMatchesResourceCollectionJsonSchema(User::class, null, 'jsonld', ['groups' => ['api-test-case-group']]);
self::createClient()->request('GET', '/users-with-groups', ['headers' => ['Accept' => $mimeType]]);
$this->assertMatchesResourceCollectionJsonSchema(User::class, null, $format, ['groups' => ['api-test-case-group']]);
}

public function testAssertMatchesResourceItemAndCollectionJsonSchemaOutputWithRangeAssertions(): void
Expand Down