Django REST framework (DRF) は Django で Web APIを構築するのに便利なパッケージです。 schema (スキーマ) を使えば OpenAPI (Swagger) のフォーマットでドキュメントを生成することができます。
この記事では、drfでどのようにスキーマが生成されるのか解説します。また、最後にスキーマの生成を自分でカスタマイズするユースケースも紹介します。
カスタマイズは魔改造な要素をふんだんに含んでいるので、こだわりがなければ最後の 3rd-party package の章で紹介しているものを使うことをおすすめします。
開発環境
OpenAPIの構成要素
OpenAPIはWeb APIを記述する仕様です。yamlやjson形式で記述します。 Swagger UI を使えば、ドキュメントとして可視化もできます。
Swagger Editor でサンプルファイルとドキュメントの出力を見ることができます。
サンプルファイルを例に、OpenAPIの構成要素を説明していきます。
infoはタイトルやdescriptionのようなメタ情報が入ります。
info: title: Swagger Petstore description: 'This is a sample server Petstore server.'
securityでは認証設定を記述します。
security: - ApiKeyAuth: [] - OAuth2: - read - write
pathsではエンドポイント (e.g. /pet
, /store
) の情報を記述します。
エンドポイントはHTTPメソッド (GET, POST, etc) ごと、Operation Objectとしてリクエストパラメーター (parameters) やレスポンス (responses) の定義ができます。
paths: /pet/{petId}: get: tags: - pet summary: Find pet by ID description: Returns a single pet operationId: getPetById parameters: - name: petId in: path description: ID of pet to return required: true schema: type: integer format: int64 responses: 200: description: successful operation content: application/xml: schema: $ref: '#/components/schemas/Pet'
componentsはpathsのレスポンスやリクエストのスキーマ定義であるcomponents objectを記述します。
例えば、上記の $ref: '#/components/schemas/Pet'
はcomponentsの以下の部分を参照します。
components: Pet: required: - name - photoUrls type: object properties: id: type: integer format: int64 category: $ref: '#/components/schemas/Category' name: type: string example: doggie photoUrls: type: array xml: name: photoUrl wrapped: true items: type: string
serversとtagsは今回扱わないので説明は省略します。 その他、詳しい仕様は Githubのドキュメント を参考にしてください。
APIをドキュメント化する方法
DRFでドキュメントを作る方法は Documenting your API - Django REST framework に記載されています。
UIとなるHTMLを設定し、 urls.py
で TemplateView
を使って表示させる方法です。
from django.views.generic import TemplateView from rest_framework.schemas import get_schema_view from .views import ConvertView schema_view = get_schema_view() # 後述 urlpatterns = [ # この `schema_view` 設定が表示される path("", schema_view, name="openapi-schema"), path( "swagger/", TemplateView.as_view( template_name="swagger-ui.html", extra_context={"schema_url": "openapi-schema"}, ) name="swagger-ui", ), path("convert/", ConvertView.as_view()), ]
TemplateView.as_view の引数 extra_context で指定している {"schema_url": "XXXX"}
は同じ url_patterns に含まれている path (今回の場合 "openapi-schema"
)を指定します。
また、ConvertViewは今回ドキュメント化の例として用いるview関数です。
schema_view
TemplateViewから呼び出す schema_view
はSchemaViewクラスである必要があります。
今回は SchemaView を簡単に設定できる get_schema_view
を使います。 ドキュメントでも紹介されている SchemaView.as_view()
を実行してくれる関数です。 (Generating a dynamic schema with SchemaView)
from rest_framework.schemas import get_schema_view schema_view = get_schema_view( title="API Lists", url="/api/", description="apiの詳細", version="1.0.0" )
OpenAPIのtitleやdescriptionのようなメタ情報はここで設定します。
ちなみに、OpenAPIとは別にcoreAPIもdrfには実装されており、特に指定しないとcoreAPIが使われます。(詳しくは後述のSchemaGeneratorで解説)
しかし、APIの中身、OpenAPIにおけるpathsやcomponentsはどこで設定できるのでしょうか? この設定をしただけではAPIの一覧が表示されるだけで、pathsのparameterやcomponentsのような詳細情報が表示されません。
公式ドキュメントではその後唐突にSchemaGeneratorやAutoSchemaの説明となってしまい、全体が掴みにくいです。 一度必要な情報を整理した後、parameterの設定をするにはどうすればいいか、解説していきます。
Schema関連クラスの関係
API全体のメタ情報を get_schema_view
で設定しましたが、実際にメタ情報を扱うのは SchemaGenerator です。
SchemaGenerator はメタ情報と pathごとの情報を集約し、スキーマを生成します。
pathごとの情報はviewクラス (APIView) に依存しています。 Viewクラスに紐づいたAutoSchemaクラスのスキーマがparameterやresponseといった情報を生成します。
図にすると以下の通りです。
ボトムアップに生成される順番を列挙すると
- viewクラスの要素AutoSchemaによって viewのpaths (parameters, response, etc.)、componentsを定義し辞書を生成する
- SchemaGeneratorがviewごとの辞書を集約し、メタ情報を含め一つの辞書にする
- SchemaViewでrendering
という流れになります。以降、SchemaGeneratorとAutoSchemaの役割について説明していきます。
SchemaGenerator
SchemaGeneratorはschema情報をまとめ、生成するクラスです。
get_schema_viewにおいて引数 generator_class
で指定できるクラスなのですが、generator_class
を渡さないと coreapi.SchemaGenerator
が指定されます。
openAPIを使いたい場合は openapi.SchemaGenerator
を指定しましょう。
メインの実行は get_schema 関数で、ドキュメントに表示するスキーマ情報を生成します。
get_schema_view
で渡される title, description などは info object として一つの要素に格納します。
ドキュメントで重要となる情報pathsとcomponentsはviewクラスごと生成します。
どのviewクラスを生成の対象にするかは get_schema_view
の引数 url_conf
で指定できます。
デフォルトのSchemaGeneratorで生成されるスキーマのobjectは openapi, info, paths, components だけなので、 security, servers なども組み込みたい場合はカスタマイズが必要です。
AutoSchema
viewクラスごと設定するpathsとcomponentsはAutoSchemaによって生成されます。
AutoSchemaは settings の DEFAULT_SCHEMA_CLASS
か、以下のようにviewクラスごとで指定できます。
from rest_framework.views import APIView class ConvertView(APIView): # (中略) schema = AutoSchema()
AutoSchemaがスキーマ情報を生成するメインの関数は get_components と get_operation です。
get_operation ではpathsの情報を生成しSchemaGeneratorに渡します。
具体的にはviewの path, methodごとparametersやresponseのような operation object の要素を定義し、辞書で返します。それぞれの定義は get_XXX
関数を使って生成します。
例えばConvertというviewクラスで、get, postの2つメソッドがある場合、以下のようにスキーマ情報が格納されます。
path["convert"]["get"] = self.get_operation("convert", "get")
path["convert"]["post"] = self.get_operation("convert", "post")
具体的な要素の指定方法を簡単に説明します。
- descriptionはmethodのdocstring (
__doc__
)から取得します。markdownに対応しているので箇条書きや太字表記も可能です。 - parametersはviewクラスのquery,setmodel,pagination_class,filter_backendsの出力を利用します。
- requestBodyはviewクラスのserializerからcomponents objectのパスを
$ref: ~
の形で指定します。 (実際にcomponents objectを生成するのはget_components
) - responsesも同様に、viewクラスのserializerからcomponents objectのパスを指定します。
get_components では components object の情報を生成しSchemaGeneratorに渡します。
主にはviewクラスが定義した serializerクラスに基づいて schema object を定義します。
このcomponentsは get_operation
の実行の際、requestBodyやresponsesなどで指定したパスと一致する仕組みになっています。
ここで、viewクラスがserializerと紐づいていないとcomponentsではなにも生成されず、スキーマ情報がほぼなくなってしまう問題が発生します。
schemaのカスタマイズ
ここまで、schemaの仕組みについて簡単に説明しました。 デフォルトのクラスをそのまま利用すると不便な点もあるため、継承した独自クラスを作ります。
SchemaGeneratorでsecurity設定をする
前述したようにデフォルトのSchemaGeneratorではsecurityを扱えません。 OpenAPIで Authorize ボタンを使えるよう、basic認証を追加してみます。
ドキュメントによると、
basic認証はcomponentsに "securitySchemes"
の設定を追加し、 security objectでも指定することでAPI全体に適用できます。
そのため、 get_schema
関数内で生成される schema
辞書にそれらの設定を追加すれば良いです。
from rest_framework.schemas.openapi import SchemaGenerator class CustomSchemaGenerator(SchemaGenerator): def get_schema(self, request=None, public=False): schema = super().get_schema(request, public) # ここから追記 if schema: if "components" in schema and "securitySchemes" not in schema["components"]: schema["components"]["securitySchemes"] = { "basicAuth": { "type": "http", "scheme": "basic", "description": "basic authentication", } } if "security" not in schema: schema["security"] = [{"basicAuth": []}] return schema
これでドキュメント表示の際にAuthorizeが表示されるようになります。 basic認証以外の認証も同様の方法で追加できます。
AutoSchemaでfilter_backendsを使ってserializerを元にparametersを設定する
記事の最初で、なにも設定しないとparametersが表示されないことがあると説明しました。 前述の通り、AutoSchemaがparametersを生成するには、viewクラスのquery,setmodel,pagination_class,filter_backendsが必要です。
querysetやmodelを使っていれば問題ないですが、serializerでrequestのデータを取得している場合、いずれにも該当せず表示されません。
今回は Django SwaggerにQuery stringの項目も出したい! の記事を参考に、 filter_backends を使って serialzer からパラメーターを生成する方法を紹介します。
AutoSchemaの get_filter_parameters の実装をみると、filter_backends.get_schema_operation_parameters
でparametersを取得しているので、これに則ったfilter_backendsクラスを自作します。
get_schema_operation_parameters
で返す要素はopenapiのドキュメント (Parameter Object) に従います。
serializerの XXXField
の型をopenapi用に変換する get_type_
も実装しました。 (今回はstring, integer, booleanだけ設定してます)
from rest_framework.fields import BooleanField, CharField, IntegerField from rest_framework.filters import BaseFilterBackend class SwaggerFilterBackend(BaseFilterBackend): @staticmethod def get_type_(value): if type(value) == CharField: type_ = "string" elif type(value) == IntegerField: type_ = "integer" elif type(value) == BooleanField: type_ = "boolean" else: type_ = "string" return type_ def get_schema_operation_parameters(self, view): """for openapi""" fields = [] for key, value in view.serializer_class._declared_fields.items(): type_ = self.get_type_(value) field = { "name": key, "required": value.required, "in": "query", "description": value.help_text, "schema": {"type": type_}, } fields.append(field) return fields
このクラスを view にセットするか、settingsの DEFAULT_FILTER_BACKENDS
でデフォルトに指定することで parametersの生成時に使うことができます。
例えば、以下のようにserializerとviewクラスを設定すると下記の画像のようなparameterが表示されます
serializers.py
from rest_framework import serializers class ConvertRequestSerializer(serializers.Serializer): text: serializers.CharField = serializers.CharField( required=False, help_text="変換したい書き言葉の文字列" ) date_replace: serializers.BooleanField = serializers.BooleanField( default=False, help_text="日付変換オプション" )
view.py
from rest_framework.views import APIView from . import serializers from .schema_utils import CustomSchema class ConvertView(APIView): http_method_names = ["get"] serializer_class = serializers.ConvertRequestSerializer schema = CustomSchema() def get(self, request): """文字列の変換"""
実際の表示
AutoSchemaでserializerを使わずresponseを設定する
serializerからrequestに必要なparametersを設定しました。
基本的に、viewクラスにはserializerクラスは1つしか設定できません。 requestで使ったserializerとは異なる定義でresponseを設定したい場合、serializer以外を使うしかありません。
AutoSchemaではserializerを使ってresponseを設定しているため、responseの取得を別の方法で設定できるようなカスタマイズしたAutoSchemaクラスも必要です。
responseも自動で生成したいところですが、今回作成したAPIはModelViewを使っていおらず、返却のスキーマを細かく決めてませんでした。 妥協策として、ドキュメントのresponse部分をコード上に書き、それをAutoSchemaに読み込ませる実装にしました。
現在はアーカイブされている django-rest-swagger の YAMLDocstringParser を参考に、viewクラスのドキュメントからyamlを取得する方法で response を設定します。
get_response
から呼び出す関数 get_yaml_from_docstring
をつくりました。
クラスのメソッドことのドキュメント __doc__
から、yamlの該当部分だけ読み込みます。
---
を起点としているのは、drfが ---
以降をopenapiのドキュメント (operation objectのdescription) として読み込まない仕様のためです。
from rest_framework.schemas.openapi import AutoSchema class CustomSchema(AutoSchema): def get_yaml_from_docstring(self, method): """docstringからyamlを取得 `---` を起点として読み込む """ view = self.view method_name = str(method).lower() if not hasattr(view, method_name): return None docstring = getattr(view, method_name).__doc__ split_lines = cleandoc(docstring).split("\n") # Cut YAML from rest of docstring for index, line in enumerate(split_lines): line = line.strip() if line.startswith("---"): cut_from = index break else: return None yaml_string = "\n".join(split_lines[cut_from:]) yaml_string = formatting.dedent(yaml_string) try: return yaml.load(yaml_string, Loader=yaml.SafeLoader) except yaml.YAMLError: return None
実際のviewクラスのドキュメントは以下のようにyamlを記述しています。
from rest_framework.views import APIView from . import serializers from .schema_utils import CustomSchema class ConvertView(APIView): http_method_names = ["get"] serializer_class = serializers.ConvertRequestSerializer schema = CustomSchema() def get(self, request): """文字列の変換 ほげほげほげほげ --- responses: schema: type: object properties: conv_text: description: '変換テキスト' type: object properties: field: description: 'kiji field or text' type: string orig_text: descrption: '元テキスト' type: object properties: field: description: 'kiji field or text' type: string """ serializer = serializers.ConvertRequestSerializer(data=request.GET.copy())
get_yaml_from_docstring
を呼び出すよう、get_response
を修正します。
ややこしいのですが、 "schema"
部分だけ欲しいので、yamlの中の response->schema だけ渡します。
また、取得したスキーマは self._get_reference(serializer)
の部分でcomponentsとして登録する ({'$ref': '#/components/schemas/responseSchema'}
のかたちになる) ので、get_components
も合わせて修正します。
具体的には、 get_response
で self.yaml_response
を保存しておき、 get_components
でこれをcomponentsとして追加します。
しない場合、登録したcomponentsは反映されません。
class CustomSchema(AutoSchema): def get_responses(self, path, method): if method == "DELETE": return {"204": {"description": ""}} self.response_media_types = self.map_renderers(path, method) serializer = self.get_serializer(path, method) if isinstance(serializer, serializers.Serializer): item_schema = self._get_reference(serializer) else: # ここから追記 yaml_res = self.get_yaml_from_docstring(method) if yaml_res and yaml_res.get("responses"): self.yaml_response = yaml_res["responses"].get("schema") item_schema = self._get_reference(serializer) else: item_schema = {} # 追記ここまで if is_list_view(path, method, self.view) and not self.yaml_response: response_schema = {"type": "array", "items": item_schema} else: response_schema = item_schema status_code = "201" if method == "POST" else "200" return { status_code: { "content": { ct: {"schema": response_schema} for ct in self.response_media_types }, } def get_components(self, path, method): if method.lower() == "delete": return {} serializer = self.get_serializer(path, method) if isinstance(serializer, serializers.Serializer): component_name = self.get_component_name(serializer) content = self.map_serializer(serializer) return {component_name: content} # ここから追記 elif self.yaml_response: component_name = self.get_component_name(serializer) return {component_name: self.yaml_response} # 追記ここまで return {}
実行すると以下のような表示になります
ちなみに、 AutoSchemaの get_responses
の実装では1つの Response Object しか設定できません。
400番台、500番台のようにcontentが必要ないのであれば以下のように追加することも可能です。
return { status_code: { "content": { ct: {"schema": response_schema} for ct in self.response_media_types }, }, "404": {"description": "Not Found", "content": {}} }
おまけ:3rd-party package
独自にカスタマイズする方法をいくつか紹介しましたが、3rd-party packageを使うという選択肢もあります。
drfのドキュメント では drf-yasg が最初に紹介されていますが、対応しているOpenAPIがver2.0と古いため、現状 drf-spectacular を利用するのが最適です。
drf-spectacular では、@extend_schema
を使ってメソッドごとパラメーターを記述できます。
paramter・responseなどの要素に対しそれぞれ別のserializerを指定することが可能です。
responseのカスタマイズで紹介したdjango-rest-swaggerはアーカイブされていますが、カスタマイズを実装する上で参考になるコードが多くあります。
OpenAPIもdrfも更新されていますので、これらのパッケージが対応しているか確認してから使いましょう。