diff --git a/src/Bridge/Symfony/Bundle/Resources/config/json_schema.xml b/src/Bridge/Symfony/Bundle/Resources/config/json_schema.xml
index d407d4d8eec..bf711ba27c5 100644
--- a/src/Bridge/Symfony/Bundle/Resources/config/json_schema.xml
+++ b/src/Bridge/Symfony/Bundle/Resources/config/json_schema.xml
@@ -22,6 +22,12 @@
+
+
+
+ %api_platform.formats%
+
+
diff --git a/src/JsonSchema/Command/JsonSchemaGenerateCommand.php b/src/JsonSchema/Command/JsonSchemaGenerateCommand.php
new file mode 100644
index 00000000000..10260483b3f
--- /dev/null
+++ b/src/JsonSchema/Command/JsonSchemaGenerateCommand.php
@@ -0,0 +1,90 @@
+
+ */
+final class JsonSchemaGenerateCommand extends Command
+{
+ private $schemaFactory;
+
+ private $formats;
+
+ public function __construct(SchemaFactoryInterface $schemaFactory, array $formats)
+ {
+ $this->schemaFactory = $schemaFactory;
+ $this->formats = $formats;
+
+ parent::__construct();
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ protected function configure()
+ {
+ $this
+ ->setName('api:json-schema:generate')
+ ->setDescription('Generates the JSON Schema for a resource operation.')
+ ->addArgument('resource', InputArgument::REQUIRED, 'The Fully Qualified Class Name (FQCN) of the resource')
+ ->addOption('itemOperation', null, InputOption::VALUE_OPTIONAL, 'The item operation')
+ ->addOption('collectionOperation', null, InputOption::VALUE_OPTIONAL, 'The collection operation')
+ ->addOption('format', null, InputOption::VALUE_OPTIONAL, 'The response format', array_key_first($this->formats))
+ ->addOption('output', null, InputOption::VALUE_NONE, 'Use this option to get the output version')
+ ;
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ protected function execute(InputInterface $input, OutputInterface $output)
+ {
+ $io = new SymfonyStyle($input, $output);
+
+ $resource = $input->getArgument('resource');
+ $itemOperation = $input->getOption('itemOperation');
+ $collectionOperation = $input->getOption('collectionOperation');
+ $format = $input->getOption('format');
+ $outputType = $input->getOption('output');
+
+ if (!isset($this->formats[$format])) {
+ throw new InvalidOptionException(\sprintf('The response format "%s" is not supported. Supported formats are : %s.', $format, implode(', ', array_keys($this->formats))));
+ }
+
+ $operationType = null;
+ $operationName = null;
+
+ if ($itemOperation && $collectionOperation) {
+ throw new InvalidOptionException('You can only use one of "--itemOperation" and "--collectionOperation" options at the same time.');
+ }
+
+ if (null !== $itemOperation || null !== $collectionOperation) {
+ $operationType = $itemOperation ? OperationType::ITEM : OperationType::COLLECTION;
+ $operationName = $itemOperation ?? $collectionOperation;
+ }
+
+ $schema = $this->schemaFactory->buildSchema($resource, $format, $outputType , $operationType, $operationName);
+
+ if (!$schema->isDefined()) {
+ $io->error(\sprintf('There is no %s defined for the operation "%s" of the resource "%s".', $outputType ? 'outputs': 'inputs', $operationName, $resource));
+ return;
+ }
+
+ $io->text(\json_encode($schema, JSON_PRETTY_PRINT | JSON_UNESCAPED_SLASHES));
+ }
+}
diff --git a/src/JsonSchema/SchemaFactory.php b/src/JsonSchema/SchemaFactory.php
index b7d890584ae..1fa825e142f 100644
--- a/src/JsonSchema/SchemaFactory.php
+++ b/src/JsonSchema/SchemaFactory.php
@@ -20,6 +20,7 @@
use ApiPlatform\Core\Metadata\Resource\Factory\ResourceMetadataFactoryInterface;
use ApiPlatform\Core\Metadata\Resource\ResourceMetadata;
use ApiPlatform\Core\Swagger\Serializer\DocumentationNormalizer;
+use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\PropertyInfo\Type;
use Symfony\Component\Serializer\NameConverter\NameConverterInterface;
use Symfony\Component\Serializer\Normalizer\AbstractNormalizer;
@@ -72,6 +73,13 @@ public function buildSchema(string $resourceClass, string $format = 'json', bool
$version = $schema->getVersion();
$definitionName = $this->buildDefinitionName($resourceClass, $format, $output, $operationType, $operationName, $serializerContext);
+
+ $method = (null !== $operationType && null !== $operationName) ? $resourceMetadata->getTypedOperationAttribute($operationType, $operationName, 'method') : 'GET';
+
+ if (!$output && !in_array($method, [Request::METHOD_POST, Request::METHOD_PATCH, Request::METHOD_PUT], true)) {
+ return $schema;
+ }
+
if (!isset($schema['$ref']) && !isset($schema['type'])) {
$ref = Schema::VERSION_OPENAPI === $version ? '#/components/schemas/'.$definitionName : '#/definitions/'.$definitionName;
diff --git a/tests/JsonSchema/Command/JsonSchemaGenerateCommandTest.php b/tests/JsonSchema/Command/JsonSchemaGenerateCommandTest.php
new file mode 100644
index 00000000000..2270afd2836
--- /dev/null
+++ b/tests/JsonSchema/Command/JsonSchemaGenerateCommandTest.php
@@ -0,0 +1,70 @@
+
+ */
+class JsonSchemaGenerateCommandTest extends KernelTestCase
+{
+ /**
+ * @var ApplicationTester
+ */
+ private $tester;
+
+ protected function setUp(): void
+ {
+ self::bootKernel();
+
+ $application = new Application(static::$kernel);
+ $application->setCatchExceptions(true);
+ $application->setAutoExit(false);
+
+ $this->tester = new ApplicationTester($application);
+ }
+
+ public function testExecuteWithoutOption()
+ {
+ $this->tester->run(['command' => 'api:json-schema:generate', 'resource' => Dummy::class]);
+
+ $this->assertJson($this->tester->getDisplay());
+ }
+
+ public function testExecuteWithItemOperationGet()
+ {
+ $this->tester->run(['command' => 'api:json-schema:generate', 'resource' => Dummy::class, '--itemOperation' => 'get']);
+
+ $this->assertJson($this->tester->getDisplay());
+ }
+
+ public function testExecuteWithCollectionOperationGet()
+ {
+ $this->tester->run(['command' => 'api:json-schema:generate', 'resource' => Dummy::class, '--collectionOperation' => 'get']);
+
+ $this->assertJson($this->tester->getDisplay());
+ }
+
+ public function testExecuteWithTooManyOptions()
+ {
+ $this->tester->run(['command' => 'api:json-schema:generate', 'resource' => Dummy::class, '--collectionOperation' => 'get', '--itemOperation' => 'get']);
+
+ $this->assertStringContainsString('You can only use one of "--itemOperation" and "--collectionOperation" options at the same time.', $this->tester->getDisplay());
+ }
+
+ public function testExecuteWithJsonldFormatOption()
+ {
+ $this->tester->run(['command' => 'api:json-schema:generate', 'resource' => Dummy::class, '--format' => 'jsonld']);
+ $result = $this->tester->getDisplay();
+
+ $this->assertStringContainsString('@id', $result);
+ $this->assertStringContainsString('@context', $result);
+ $this->assertStringContainsString('@type', $result);
+ }
+}