API PlatformのOpenAPI生成で、エンティティのidをrequiredにする
API PlatformのOpenAPI生成で、nullableなエンティティプロパティの型をanyOfではなくoneOfで出力する の続きというか関連記事です。
背景
API Platform によって自動生成されたOpenAPIにおいて、Doctrineエンティティの id プロパティはなぜか正規の手順では required にできません。
正規の手順としては、公式ドキュメント を参考に
#[ApiProperty(required: true)]
private ?string $name = null;
とアトリビュートをつけるだけでプロパティを required としてOpenAPIを生成してくれます。
アトリビュートではなくYAMLで設定する場合は以下のように書きます。
App\Entity\Foo: properties: name: required: true
しかし、Doctrineエンティティの id プロパティだけは、なぜかこの方法を使っても required として出力されてくれません。これは困ります。
具体的にAPI Platformの実装のどの部分が原因で、それが意図した仕様なのかバグなのかなど細かいことは何も調べられていません🙏
API Platformのバージョンは記事執筆時点で最新の安定版である 2.6.8 です。
API Platformのコードにおける原因箇所
API Platformのコードをgrepやvar_dumpを駆使して調べたところ、
- https://github.com/api-platform/core/blob/2.6/src/JsonSchema/SchemaFactory.php
- https://github.com/api-platform/core/blob/2.6/src/Hydra/JsonSchema/SchemaFactory.php
この2ファイルがJSONおよびJSON-LD(Hydra)それぞれのOpenAPIのスキーマを生成していることが分かりました。
API PlatformのOpenAPI生成で、nullableなエンティティプロパティの型をanyOfではなくoneOfで出力する
でも似たような対処をしましたが、今回もこの SchemaFactory を デコレート して拡張した自作サービスに差し替えてあげることで対応できそうです。
なお、この辺り を見ると、JSON-LD用の SchemaFactory が "jsonld" 以外のフォーマット向けに呼ばれたときは、JSON用 SchemaFactory の結果をそのまま返すという実装になっているので、JSON-LDを有効にしているアプリではJSON-LD用の SchemaFactory を差し替えてその戻り値を加工してあげればよさそうです。
 API Platformの SchemaFactory を拡張する
vendor/api-platform 配下を ApiPlatform\Core\Hydra\JsonSchema\SchemaFactory でgrepするなり、Symfonyプラグインを入れたPhpStormなど補完が強力な環境で services.yaml 上で適当に apiplatformschemafactory などとタイプしてみると、このクラスが api_platform.hydra.json_schema.schema_factory というサービスIDでSymfonyにサービスとして登録されていることが分かります。
なので、Symfonyのサービスデコレート機能 を使って以下のようにサービスを差し替えてあげればよさそうです。
# config/services.yaml
services:
  App\ApiPlatform\SchemaFactory: # というクラスを自作する
    decorates: api_platform.hydra.json_schema.schema_factory
肝心の自作するクラスの内容ですが、
API PlatformのOpenAPI生成で、nullableなエンティティプロパティの型をanyOfではなくoneOfで出力する
と同様今回のクラスも final クラスなので、継承して部分的に処理を変更することはできず、最終的な戻り値を加工してあげるしか方法はありません。
元の SchemaFactory クラスの buildSchema() メソッドの処理をvar_dumpなどしながら確認してみると、
- 
ApiPlatform\Core\JsonSchema\Schemaクラスのインスタンスを返す
- 
Schema::getDefinitions()でスキーマの\ArrayObjectが得られる
- 各スキーマの propertiesカラムおよびrequiredカラムにプロパティの内容と必須かどうかの定義がある
ということが分かります。
というわけで、自作クラスの内容は以下のようになりました。
<?php
declare(strict_types=1);
namespace App\ApiPlatform;
use ApiPlatform\Core\JsonSchema\Schema;
use ApiPlatform\Core\JsonSchema\SchemaFactoryInterface;
/**
 * @see \ApiPlatform\Core\Hydra\JsonSchema\SchemaFactory
 */
final class SchemaFactory implements SchemaFactoryInterface
{
    public function __construct(private SchemaFactoryInterface $decorated)
    {
    }
    public function buildSchema(string $className, string $format = 'json', string $type = Schema::TYPE_OUTPUT, ?string $operationType = null, ?string $operationName = null, ?Schema $schema = null, ?array $serializerContext = null, bool $forceCollection = false): Schema
    {
        $schema = $this->decorated->buildSchema($className, $format, $type, $operationType, $operationName, $schema, $serializerContext, $forceCollection);
        $definitions = $schema->getDefinitions();
        if ($key = $schema->getRootDefinitionKey()) {
            if (isset($definitions[$key]['properties']['id']) && !in_array('id', $definitions[$key]['required'] ?? [], true)) {
                $definitions[$key]['required'][] = 'id';
            }
        }
        return $schema;
    }
}
結果
上記のとおり自作クラスを書いてサービスを decorates によって差し替えた結果、API Platformが生成するOpenAPIの内容は
  {
    "components": {
      "schemas": {
        "Foo": {
          "type": "object",
          "required": [
            "既存の必須プロパティ",
            :
            :
+           "id"
          ],
          "properties": {
            :
            :
          }
        },
      },
    },
  }
という感じで期待どおり id プロパティを required に追加することができました。
SwaggerUI上でも、


こんな感じでちゃんと必須になっています。(余計なプロパティはdevtoolで消してあります)
めでたしめでたし🍵




Discussion