Welcome to restframework-definable-serializer

restframework-definable-serializerはYAMLやJSONを用いてdjango-restframeworkのシリアライザークラスを動的に作成することができます。

このライブラリがどのようなものかを知りたい方は restframework-definable-serializerとは を参照してください。

シリアライザーの定義方法については シリアライザーの定義方法 を参照してください。

restframework-definable-serializerを利用することで、少しでもdjangoを利用するエンジニアの手間が減ることを願っています。


インストール方法

パッケージのインストール

以下のコマンドを実行してインストールしてください:

pip install --upgrade restframework-definable-serializer

警告

もしdjangoとrestframeworkがインストールされていない場合は先にインストールを行ってください:

pip install --upgrade django djangorestframework

INSTALLED_APPSへの追加

settings.py中にある INSTALLED_APPScodemirror2definable_serializer を追加してください。:

INSTALLED_APPS = (
    ...
    'rest_framework',
    'codemirror2',
    'definable_serializer',
)

LocaleMiddlewareの追加

フィールドの国際化 を利用する場合は settings.py中にある MIDDLEWARELocaleMiddleware を追加してください。:

MIDDLEWARE = (
    ...
    "django.middleware.locale.LocaleMiddleware",
    ...
)

restframework-definable-serializerとは

概要

django-restframework(以下restframework)のもつ、djangoモデル(以下モデル)からシリアライザーを生成できるモデルシリアライザーはとても強力です。 このおかげでシリアライザー用のコードを書く手間を省くことができます。

しかしモデルシリアライザーから作成したシリアライザーのフィールド変更は、即ちモデルのフィールドを変更と同義です。ひとたび変更が発生すれば、デプロイとマイグレーション作業を行わなければなりません。

しかもマイグレーションを伴うデプロイはひと気がない深夜に行わなければならない場合が多く、これが続くとエンジニアは辟易してしまいます。

例えば名前、年齢、性別だけを扱うアンケートがあるとしましょう。 最初はこれでよかったはずが、顧客からの要望で入力項目がどんどんと増えていき、最後にはかなりの項目数になっていた…。 筆者はこれに近い経験をしたことがあります。これは非常に苦痛でした。

この経験から学べることは 変更が多く発生するシリアライザーはモデルシリアライザーから作成してはいけない ということです。

シリアライザーのフィールドとモデルのフィールドが対になっているが故に、シリアライザーのフィールドが変更されるたびにモデルの変更が発生してしまうのです。

この問題を解決するには 手軽に変更可能な定義からシリアライザーを動的に作る ことです。

definable-serializerは、YAML/JSON(ファイル/文字列)からシリアライザーを作ることができます。

また、シリアライザーの定義を行うためのモデルフィールドや、ユーザーからの入力情報を保存するフィールドなども提供しています。


definable-serializerができること

definable-serializerは以下のような機能を備えています。

  • YAML/JSON(ファイル/文字列)からdjango-restframeworkで利用可能なシリアライザーを作成する機能を提供します
  • adminサイトでシリアライザーを定義するためのモデルフィールドと、記述されたシリアライザーを確認する機能を提供します
  • ユーザーからの入力を保存するためのモデルフィールドを提供します
  • TemplateHTMLRendererを利用する際に便利なシリアライザーフィールドを提供します(将来的に分離予定です)

文字列やファイルからシリアライザーを作ることもできますが、特にadmin画面でシリアライザーの定義が記述されることを期待しています。

_images/define_by_admin_display.png

adminサイトで記述したシリアライザー定義と確認


シリアライザー定義の記述例

以下にYAMLで定義した簡単なシリアライザーの例を紹介します。

main:
  name: EnqueteSerializer
  fields:
  - name: name
    field: CharField
    field_kwargs:
      required: true
      max_length: 100
  - name: age
    field: IntegerField
    field_kwargs:
      required: true
  - name: gender
    field: ChoiceField
    field_args:
    - - - male
        - 男性
      - - female
        - 女性
    field_kwargs:
      required: true

上の定義は名前、年齢、性別の3つの入力を持つシリアライザーの例です。 この定義をdefinable-serializerを用いてシリアライザー化すると以下のようになります。

EnqueteSerializer():
    name = CharField(max_length=100, required=True)
    age = IntegerField(required=True)
    gender = ChoiceField([['male', '男性'], ['female', '女性']], required=True)

これをrestframeworkの持つBrowsableAPIRendererで表示すると以下の様になります。

_images/browse_enquete_serializer.png

BrowsableAPIRendererで表示した例


YAMLで記述された定義からシリアライザーを作成する

実際にYAMLデータから名前を扱う簡単なシリアライザーを作成し、データを入力してバリデーションを行います。

djangoシェルを立ち上げて以下のように打ち込んでみましょう。:

./manage.py shell

djangoのシェルが立ち上がったら以下のコードを実行してみましょう

>>> from definable_serializer.serializers import build_serializer_by_yaml

# 名前だけを扱うシリアライザーのYAML定義
>>> YAML_DEFINE_DATA = """
... main:
...   name: YourFirstSerializer
...   fields:
...   - name: name
...     field: CharField
...     field_kwargs:
...       required: true
...       max_length: 100
... """

# シリアライザー化
>>> serializer_class = build_serializer_by_yaml(YAML_DEFINE_DATA)
>>> serializer_class()
FirstSerializer():
    name = CharField(max_length=100, required=True)

# バリデーション成功例
>>> serializer = serializer_class(data={"name": "Taro Yamada"})
>>> serializer.is_valid()
>>> serializer.validated_data
OrderedDict([('name', 'Taro Yamada')])

# バリデーションエラー例(空の場合)
>>> serializer = serializer_class(data={"name": ""})
>>> serializer.is_valid()
False
>>> serializer.errors
{'name': ['This field may not be blank.']}

# バリデーションエラー例(100文字を超えていた場合 )
>>> serializer = serializer_class(data={"name": "a" * 101})
>>> serializer.is_valid()
False
>>> serializer.errors
{'name': ['Ensure this field has no more than 100 characters.']}

このように、YAMLで記述された定義からシリアライザーを作成することができました。 次はアンケートを扱うexampleアプリケーションを作成し、definable-serializerをadminサイトへを組み込む例を紹介するとともに、ユーザー側のビューを作成する例も紹介します。

プロジェクトにdefinable-serializerを組み込む

definable-serializerの一番の目的は、シリアライザーの入力項目の変更を容易にし、デプロイ作業からエンジニアを守ることです。 故にシリアライザーの定義をファイルに書いては意味がありません。

その問題を解決する一番の方法はWebインターフェイスです。ウェブインターフェイスからシリアライザーの定義を変更することができれば、デプロイを行わずに済みます。 我々はrestframeworkを利用している時点でdjangoを利用しており、djangoはadminサイトにてモデルの追加/変更を簡単に行うことができます。

これを利用してモデルにシリアライザー定義用のフィールドを追加すれば簡単に定義を変更することができます。

definable-serializerではシリアライザー定義を扱うためのモデルフィールドを提供しています。


シリアライザーを定義するためのモデルフィールド

definable-serializerではモデルにてYAML/JSONでシリアライザー定義を扱うための DefinableSerializerByYAMLFieldDefinableSerializerByJSONField というモデルフィールドを用意しています。

これらのフィールドを利用すると、adminサイト中にCodeMirror2を使ったテキストエリアが現れます。

さらにadminサイト中で記述されたシリアライザーの DefinableSerializerAdmin クラスを提供しています。 ここでは簡単なプロジェクトを作成し、definable-serializerの組み込み例を紹介します。


アンケートシステムを作成してadmin画面でシリアライザーを定義する

簡単なアンケート(Survey)を取るためのシステムがあるとします。

しかしこのアンケートシステムの営業担当は顧客に対して寛容な心を持ち、顧客の要望全てに答えようとしてしまいます。 担当のエンジニアは変更のたびにモデルフィールドの追加/削除を求められ、挙句の果てにラベルやヘルプテキストの変更などありとあらゆる要望に答えならなくなったとします。

そんなときこそdefinable-serializerが真価を発揮します。

ここではアンケートをとるためのプロジェクトを作成し、definable-serializeの組み込み方を説明します。

警告

ここではある程度djangoとrestframeworkの扱いを知っている方を対象とします。 また、インストール方法 を読んでいない方は先に読んで準備を整えてください。

この説明中で記載するコードは完全に動作するものではありません。実際に動作するものを確認したい場合は、 完全に動作するサンプルプロジェクトを用意しています。 ので、そちらを参照してください。

exampleプロジェクトとsurveysアプリケーションの作成

適当なディレクトリ上で以下のコマンドを実行し、exampleプロジェクトとsurveysアプリケーションを作成します。

$ django-admin.py startproject example_projecet
$ cd ./example_projecet
$ ./manage.py startapp surveys

次に settings.py 中の INSTALLED_APPS を変更します。

INSTALLED_APPS = (
    ...
    'rest_framework',
    'codemirror2',
    'definable_serializer',
    'surveys',
)

surveysのmodels.pyとadmin.pyの変更

models.py では質問を取り扱う Survey モデルと、回答データを扱うための Answer モデルを用意します。

Surveyモデル
Surveyモデルには先ほど紹介した DefinableSerializerByYAMLField を利用して質問用のシリアライザー定義を取り扱う question フィールドと、 アンケートタイトルを扱う title フィールドを追加します。
Answerモデル
Answerモデルには回答対象へリレーションを張るための survey フィールドと、 回答データを保持する answer フィールドを追加します。

admin.py ではSurveyモデル、およびAnswerモデルをAdminサイトで確認できるように変更を行います。

models.pyを変更する

surveys/models.py を変更します。

Surveyモデルは、models.Model ではなく AbstractDefinitiveSerializerModel を継承している点に注意してください。

# surveys/models.py
from django.db import models
from django.conf import settings
from definable_serializer.models import (
    DefinableSerializerByYAMLField,
    AbstractDefinitiveSerializerModel,
)
from definable_serializer.models.compat import YAMLField


class Survey(AbstractDefinitiveSerializerModel):
    title = models.CharField(
        null=False,
        blank=False,
        max_length=300,
    )

    # YAMLで定義されたシリアライザーを扱うフィールド
    question = DefinableSerializerByYAMLField()

    def __str__(self):
        return self.title


class Answer(models.Model):
    survey = models.ForeignKey("Survey")

    respondent = models.ForeignKey(
        settings.AUTH_USER_MODEL,
        on_delete=models.CASCADE,
    )

    answer = YAMLField(
        null=False,
        blank=False,
        default={},
        verbose_name="answer data",
        help_text="answer data"
    )

    class Meta:
        unique_together = ("survey", "respondent",)
admin.pyを変更する

admin画面にsurveyモデル,及びAnswerモデルを確認/変更するページを表示するため、 surveys/admin.py を変更します。 SurveyAdminクラスは、admin.ModelAdminではなく、 DefinableSerializerAdmin を継承している点に注意してください

# surveys/admin.py
from django.contrib import admin
from definable_serializer.admin import DefinableSerializerAdmin
from surveys import models as surveys_models

@admin.register(surveys_models.Survey)
class SurveyAdmin(DefinableSerializerAdmin):
    list_display = ("id", "title",)
    list_display_links = ("id", "title",)


@admin.register(surveys_models.Answer)
class AnswerAdmin(admin.ModelAdmin):
    list_display = ("id", "survey", "respondent",)
    list_display_links = ("id", "survey",)

作業が完了するとadminサイトにSurveyモデルとAnsweモデルの変更を行うページが追加されます。

質問用のシリアライザー定義を記述する

adminサイトを確認するために開発用サーバーを起動します。初回起動のため、マイグレーション作業及びadminアカウントを作成した後に開発用サーバーを起動します。

$ ./manage.py makemigrations
...

$ ./manage.py migrate
...

$ ./manage.py createsuperuser
Username (leave blank to use 'your-name'): admin
Email address: admin@example.com
Password: <password>
Password (again): <password>
Superuser created successfully.

$ ./manage.py runserver 0.0.0.0:8000
Django version 1.11.6, using settings 'example_project.settings'
Starting development server at http://0.0.0.0:8000/
Quit the server with CONTROL-C.

起動したら http://localhost:8000/admin/surveys/survey/add/survey をブラウザーで開いてSurveyモデルのadmin画面にアクセスしましょう。

タイトルとYAMLで記述されたシリアライザー定義を入力します。ここでは名前、年齢、性別の3つを扱う簡単なシリアライザーを定義します。 以下のYAMLデータをquestionのフィールドにコピー&ペーストしてください。(タイトルは適当で構いません)

main:
  name: EnqueteSerializer
  fields:
  - name: name
    field: CharField
    field_kwargs:
      required: true
      max_length: 100
  - name: age
    field: IntegerField
    field_kwargs:
      required: true
  - name: gender
    field: ChoiceField
    field_args:
    - - - male
        - 男性
      - - female
        - 女性
    field_kwargs:
      required: true

入力が完了したら、[保存して編集を続ける]ボタンを押します。すると、編集画面の上部に定義したシリアライザーのクラス情報が表示されます。

_images/survey_admin_editing.png

保存後に問題がなければシリアライザークラスの情報がページ上部に表示されます。

また、定義されたシリアライザーをrestframeworkのもつBrowsable APIのページを使って確認することもできます。

タイトルラインにある [Show Restframework Browsable Page] のリンクをクリックすると、 Browsable APIのページが開き、定義したシリアライザーの入力テストを行うことができます。

_images/serializer_with_browsable_api.png

Browsable APIで確認した例

シリアライザーを確認できたところで、次は定義を変更してみましょう。例として紹介文用のフィールド、 introduction を追加します。

main:
  name: EnqueteSerializer
  fields:

  ...

  - name: introduction
    field: definable_serializer.extra_fields.TextField
    field_args:
      required: true
      placeholder: Hello!

追加が完了したらモデルを保存して、再度 Browsable APIのページでシリアライザーの状態を確認してみましょう。 問題がなければ、テキストエリアが追加されます。

_images/add_textarea_to_serializer_with_browsable_api.png

定義が正しければテキストエリアが追加されます

次はユーザーがアンケートの回答および入力内容の変更/確認を行うビューを作成します。


ユーザーからの回答を受け付けるビューの作成

restframeworkを利用する場合、REST API経由でやり取りをするケースが多いと思いますが、 ここではrestframeworkが持つ TemplateHTMLRenderer も同時にサポートしてユーザーの回答用ビューを作成します。

このビューにおいて問題になるのが、Surveyモデルオブジェクト中のシリアライザー定義からシリアライザークラスを取り出す方法と、 POSTされた回答内容をどのように保存するかという点です。

definable-serializerではこれらの問題を解決するための方法を提供しています。

モデルからシリアライザークラスを取り出す方法

シリアライザー定義用フィールドを持つモデルオブジェクトからシリアライザークラスを取り出すのはさほど難しくありません。

先ほど定義したSurveyモデルは AbstractDefinitiveSerializerModel を継承しており、 シリアライザークラスを取り出すためのメソッドである get_question_serializer_class がモデルオブジェクトに自動で追加されるからです。

例として先ほど作成したSurveyモデルオブジェクトから question フィールドに記述したシリアライザー定義の シリアライザークラスを取り出します。

>>> from surveys import models as surveys_models
>>> survey_obj = surveys_models.Survey.objects.get(pk=1)
>>> question_serializer_class = survey_obj.get_question_serializer_class()
>>> question_serializer = question_serializer_class()
>>> print(question_serializer)
EnqueteSerializer():
    name = CharField(max_length=100, required=True)
    age = IntegerField(required=True)
    gender = ChoiceField([['male', '男性'], ['female', '女性']], required=True)
    introduction = TextField(placeholder='Hello!', required=True)

ヒント

例えば foobar というモデルフィールドが DefinableSerializerByYAMLField または DefinableSerializerByJSONField のどちらかを利用していたら、 get_foobar_serializer_class というメソッド名でシリアライザークラスを取り出すことができます。 (ただし、モデルが AbstractDefinitiveSerializerModel を継承している場合のみに限ります)

入力された内容を保存する方法

definable-serializerでは、シリアライザーのフィールドとモデルのフィールドを対にしないという理念のもと作られています。 そのためシリアライザーに渡されたユーザーからの入力内容は、モデルの単一のフィールドにJSON/YAML/Pickle等にシリアライズ(直列化)して保存する必要があります。

definable-serializerでは、ユーザーからの入力を保存するために JSONFieldYAMLField を用意しています。 先ほど作成したmodels.py中のAnswerモデルのanswerフィールドは YAMLField を利用しています。

以下にAnswerモデルに追加したanswerフィールドにアンケートの内容を保存するためのコード例を示します。

# シリアライザークラスを作成してデータを渡し、バリデーションを行う
>>> from surveys import models as surveys_models
>>> survey_obj = surveys_models.Survey.objects.get(pk=1)
>>> question_serializer_class = survey_obj.get_question_serializer_class()
>>> question_serializer = question_serializer_class(data={
...     "name": "John Smith",
...     "age": 20,
...     "gender": "male",
...     "introduction": "Hi!"
... })
>>> question_serializer.is_valid()
True

>>> from django.contrib.auth import get_user_model
>>> admin_user = get_user_model().objects.get(pk=1)
>>> print(admin_user)
admin
>>> answer_obj = surveys_models.Answer.objects.create(
...     survey=survey_obj,
...     respondent=admin_user,
...     answer=question_serializer.validated_data
... )
>>> answer_obj.answer
odict_values(['John Smith', 20, 'male', 'Hi!'])

実際に入れたデータをadminサイトで確認してみましょう。YAML形式で保存されていることが確認できます。

_images/data_store_by_yaml.png

!!Ordered Mapping で保存されていることが確認できます。

ヒント

例としてYAMLFieldを用いてバリデーション後の結果を保存しましたが、モデルフィールドさえ提供されていれば、色々な形式で保存することが出来ます。 詳しくは ユーザーからの入力データを保存するモデルフィールド を参照してください

警告

保存したJSONデータを検索の対象としたい場合はdjangoの提供する django.contrib.postgres.fields.JSONField を利用することを強くおすすめします。 ただし、そのままではいくつかの問題があります。詳しくは JSONFieldでデータを扱う場合の問題点 を御覧ください。

ユーザー回答用ビューの作成例

上の内容を踏まえて回答用ビューの作成例を示します。

警告

下記に示すコードは作成例です。 urls.pyへの登録、テンプレートの用意、登録後のリダイレクト先が存在しない等の問題により、このままでは正しく動作しません。 ここではそれらが完全に揃っていることにして説明を続けます。

実際に動作するものを確認したい場合は 完全に動作するExampleプロジェクトを用意しています

from django.contrib import messages
from django.http import HttpResponseRedirect
from django.shortcuts import get_object_or_404

from rest_framework.views import APIView
from rest_framework.response import Response
from rest_framework.renderers import TemplateHTMLRenderer, JSONRenderer
from rest_framework.exceptions import MethodNotAllowed, NotFound
from rest_framework.permissions import IsAuthenticated
from rest_framework.authentication import (
    SessionAuthentication, TokenAuthentication
)

from . import models as surveys_models


class Answer(APIView):
    """
    Answer API
    """
    allowed_methods = ("GET", "POST", "OPTIONS",)
    renderer_classes = (TemplateHTMLRenderer, JSONRenderer,)
    authentication_classes = (SessionAuthentication, TokenAuthentication,)
    permission_classes = (IsAuthenticated,)
    template_name = 'answer.html'

    def _get_previous_answer(self, survey):
        """
        過去の回答データを取得します。存在しない場合はNoneを返します
        """
        previous_answer = None
        try:
            previous_answer = surveys_models.Answer.objects.get(
                respondent=self.request.user, survey=survey)
        except surveys_models.Answer.DoesNotExist:
            pass

        return previous_answer

    def initial(self, request, *args, **kwargs):
        super().initial(request, *args, **kwargs)
        survey = get_object_or_404(
            surveys_models.Survey, pk=kwargs.get('survey_pk'))
        self.previous_answer = self._get_previous_answer(survey)
        self.survey = getattr(self.previous_answer, "survey", None) or survey

    def get_serializer(self, *args, **kwargs):
        """
        質問用のシリアライザークラスを返します
        """
        return self.survey.get_question_serializer_class()(*args, **kwargs)

    def get(self, request, survey_pk, format=None):
        """
        Request HeaderのAcceptが "application/json" の場合はJSONRendererで
        過去の入力データを返します。回答がない場合は404を返します。

        Request HeaderのAcceptが "application/json" 以外の場合、質問の入力画面を表示します。
        ユーザーが過去に同じ質問に回答していた場合、回答データを復元して表示します。
        """
        response = None
        serializer = self.get_serializer()
        if self.previous_answer:
            serializer = self.get_serializer(data=self.previous_answer.answer)
            serializer.is_valid()

        if isinstance(self.request.accepted_renderer, TemplateHTMLRenderer):
            response = Response(
                {'serializer': serializer, 'survey': self.survey})
        else:
            if not self.previous_answer:
                raise NotFound()
            response = Response(serializer.data)

        return response

    def post(self, request, survey_pk):
        """
        回答データの投稿を受け付けます。入力内容に不備があった場合はそれぞれのレンダラーでエラーレスポンスを返します。

        回答データに問題がなく、TemplateHTMLRendererを利用する場合はトップ画面にリダイレクトします。
        JSONRendererの場合は成功レスポンスを返します。

        また、過去に投稿がない場合は新しくAnswerオブジェクトを作成し、投稿があった場合はAnswerオブジェクトを更新します。
        """
        response = None
        serializer = self.get_serializer(data=self.request.data)

        if isinstance(self.request.accepted_renderer, TemplateHTMLRenderer):
            response = HttpResponseRedirect("/")
            if not serializer.is_valid():
                response = Response(
                    {'serializer': serializer, 'survey': self.survey})
            else:
                messages.add_message(
                    request, messages.SUCCESS, 'Thank you for posting! 💖')
        else:
            serializer.is_valid(raise_exception=True)
            response = Response(serializer.data)

        if serializer.is_valid():
            if self.previous_answer:
                self.previous_answer.answer = serializer.validated_data
                self.previous_answer.save()
            else:
                surveys_models.Answer.objects.create(
                    survey=self.survey,
                    respondent=request.user,
                    answer=serializer.validated_data
                )

        return response

    def options(self, request, *args, **kwargs):
        """
        APIスキーマやその他のリソース情報を返します。
        ただし、Request HeaderのAcceptが "text/html"の場合は 405(Method Not Allowed)を返します。
        """
        if request.accepted_media_type == TemplateHTMLRenderer.media_type:
            raise MethodNotAllowed(
                "It can not be used except when "
                "it is content-type: application/json."
            )
        return super().options(request, *args, **kwargs)

回答用ビューのアクセス例

ブラウザーでレスポンスを得た場合

上記のビューにブラウザーからアクセスするとHTMLTemplateRendererにより、以下のようなレスポンスを返します。

_images/survey_answer_view_with_browser.png

回答画面のイメージ

Postmanを用いてREST API経由のレスポンスを得た場合

Chromeの機能拡張であるPostman を用いてREST API経由で回答を行った場合の例を示します。

_images/survey_answer_view_with_postman.png

警告

REST API経由でアクセスを行う場合は、Headersタブにて Accept, Authorization, Content-Type の3つを適切に指定してください。

_images/postman_with_headers.png
Postmanを用いてOPTIONSメソッドでレスポンスを得た場合

OPTIONS メソッドでアクセスするとREST APIの詳細情報及びPOST時のJSONスキーマが表示されます。

以下にレスポンス例を示します。

{
    "name": "Answer",
    "description": "Answer API",
    "renders": [
        "text/html",
        "application/json"
    ],
    "parses": [
        "application/json",
        "application/x-www-form-urlencoded",
        "multipart/form-data"
    ],
    "actions": {
        "POST": {
            "name": {
                "type": "string",
                "required": true,
                "read_only": false,
                "label": "Name",
                "max_length": 100
            },
            "age": {
                "type": "integer",
                "required": true,
                "read_only": false,
                "label": "Age"
            },
            "gender": {
                "type": "choice",
                "required": true,
                "read_only": false,
                "label": "Gender",
                "choices": [
                    {
                        "value": "male",
                        "display_name": "男性"
                    },
                    {
                        "value": "female",
                        "display_name": "女性"
                    }
                ]
            },
            "introduction": {
                "type": "string",
                "required": true,
                "read_only": false,
                "label": "Introduction"
            }
        }
    }
}

シリアライザーの定義方法

restframeworkのシリアライザーは、システムからの出力とユーザーからの入力を適切に処理するために存在します。 これはdjangoのフォームと変わりません。しかし、djangoの提供するフォームはネストした複雑なデータの取り扱いには向いていません。

それに比べ、restframeworkのシリアライザーはネストされたデータも上手に扱うことができるため、djangoのformよりもパワフルです。 djangoのフォームも、formsets等を利用して複数のフォームを並べることもできますが、UI/UXの観点からみると絶望的な状況になるでしょう。

ここではdefinable-serializerを用いて単純なシリアライザーと、ネストされたシリアライザーをYAMLで定義する方法を解説します。

またシリアライザーが持つフィールドの記述方法と、validateメソッドを含むシリアライザーの記述方法についても解説します。


単純なシリアライザーとネストされたシリアライザー

単純なシリアライザー

単純なシリアライザーとは、フィールド中に別のシリアライザーをネストしていないものを指します。 例えばファーストネームとラストネームのみを扱うような構造のシリアライザーの場合は以下のように記述します。

main:
  name: NameEntry
  fields:
  - name: first_name
    field: CharField
    field_kwargs:
      required: true
      max_length: 100

  - name: last_name
    field: CharField
    field_kwargs:
      required: true
      max_length: 100

この定義をシリアライザークラス化すると以下のようになります。

NameEntry():
    first_name = CharField(max_length=100, required=True)
    last_name = CharField(max_length=100, required=True)

このシリアライザーに渡せるデータは以下のような形式になります。

{
    "first_name": "John",
    "last_name": "Smith",
}

ネストされたシリアライザー

ネストされたシリアライザーとは、シリアライザー中に他のシリアライザーを含むものを指します。 例えばSNSのようにグループに人を紐付けるような構造のシリアライザーがそれにあたります。

以下の記述例を示します。

main:
  name: Group
  fields:
  - name: group_name
    field: CharField
    field_kwargs:
      label: Group name
      required: true

  - name: persons
    field: Person
    field_kwargs:
      many: true

depending_serializers:
- name: Person
  fields:
  - name: first_name
    field: CharField
    field_kwargs:
      required: true

  - name: last_name
    field: CharField
    field_kwargs:
      required: true

この定義をシリアライザークラス化すると以下のようになります。

Group():
    group_name = CharField(label='Group name', required=True)
    persons = Person(many=True):
        first_name = CharField(required=True)
        last_name = CharField(required=True)

このシリアライザーに渡せるデータは以下のような形式になります(Persons部分がネストされたシリアライザーになります)。

{
    "group_name": "My dearest friends",
    "persons": [
        {"first_name": "John", "last_name": "Smith"},
        {"first_name": "Taro", "last_name": "Yamada"}
    ]
}

ここで注目するべきは、定義中の depending_serializers の項目です。 この項目は、mainのシリアライザーを作成する前に予め作成されるシリアライザークラスのリストになります。

depending_serializers 中で先にシリアライザーの定義がされていれば、後に記述されるシリアライザーはそれらを利用することができます。

main:
  name: YourFavorite
  fields:
  - name: foods_and_animal
    field: FoodsAndAnimals
    field_kwargs:
      many: true

depending_serializers:
- name: Animal
  fields:
  - name: name
    field: CharField
- name: Food
  fields:
  - name: name
    field: CharField
- name: FoodsAndAnimals
  fields:
  - name: animals
    field: Animal   # 上で定義されているAnimalを利用しています
    field_kwargs:
      many: true
  - name: foods     # 上で定義されているFoodを利用しています
    field: Food
    field_kwargs:
      many: true

この定義をシリアライザークラス化すると以下のようになります。

YourFavorite():
    foods_and_animal = FoodsAndAnimals(many=True):
        animals = Animal(many=True):
            name = CharField()
        foods = Food(many=True):
            name = CharField()

シリアライザーフィールドの記述方法

シリアライザーにはフィールドが必要です。フィールドを記述するにはフィールド名とフィールドタイプを必ず指定します。 任意でフィールドの引数、名前付き引数を指定することができます。以下にフィールドの記述例を示します。

- name: gender          # フィールド名
  field: ChoiceField    # フィールドタイプ(フィールドクラス名)
  field_args:           # フィールドタイプの引数(list)
  - - - male
      - 男性
    - - female
      - 女性
  field_kwargs:         # フィールドタイプの名前付き引数(dict)
    required: true
    label: 性別を入力してください

上記の定義は以下のPythonコードと同義になります。

>>> from rest_framework import serializers
>>> gender = serializers.ChoiceField(
...     [["male", "男性"], ["female", "女性"]],
...     required=True,
...     label="性別を入力してください"
... )
>>> gender
ChoiceField([['male', '男性'], ['female', '女性']], label='性別を入力してください', required=True)

restframeworkが提供するシリアライザーフィールドの利用

警告

definable-serializerでは DictField, ListField 及び SerializerMethodField 以外のシリアライザーフィールドが利用可能です。(これらのフィールドは将来的にサポートされる予定です)

definable-serializerではrestframeworkが提供するほとんどのシリアライザーフィールドを利用することができます。 シリアライザーフィールドの一覧については restframeworkのシリアライザーフィールドのページを参照してください

restframeworkが提供するシリアライザーフィールドを利用する場合はクラス名だけを指定します。

- name: my_checkbox    # フィールド名
  field: BooleanField  # フィールドタイプ

- name: my_char        # フィールド名
  field: CharField     # フィールドタイプ

- name: my_regex_field # フィールド名
  field: RegexField    # フィールドタイプ
  field_args:
  - a-zA-Z0-9

サードパーティパッケージが提供するシリアライザーフィールドの利用

definable-serializerではサードパーティパッケージ、つまりrestframework以外が提供するシリアライザーフィールドも利用可能です。 利用する場合は、各フィールド定義の field 項目 に <パッケージ名>.<モジュール名>.<クラス名> の形式で指定します。

main:
  name: AgreementSerailizer
  fields:
  - name: agreement
    field: definable_serializer.extra_fields.CheckRequiredField # サードパッケージが利用するシリアライザーフィールド
    field_kwargs:
      initial: false

この定義をシリアライザークラス化すると以下のようになります。

IncludeExtraSerializerField():
    agreement = CheckRequiredField()

ヒント

definable-serializerでは TemplateHTMLRendererに向けて、いくつかのシリアライザーフィールドを提供しています。 提供するシリアライザーフィールドクラス を御覧ください

フィールドの国際化

0.1.12で登場しました

djangoは国際化機構を提供しているため、システム全体の翻訳を行うことが可能です。

しかしこの機構はgettextを利用するため、コンパイルされた翻訳ファイルをサーバーにデプロイする必要があります。 definable-serializerの目的はデプロイの手間を減らすことなので、残念ながらこの機構を利用するわけにはいきません。

この問題を回避する方法として、localeと翻訳テキストを対にした辞書を翻訳が必要なフィールドに指定し、ユーザーのlocaleを元に翻訳テキストに切り替える方法を利用します。 翻訳のテキストを指定出来るフィールドは引数中の以下の項目になります。

  • choices 引数
  • label キーワード引数
  • help_text キーワード引数
  • initial キーワード引数
  • style キーワード引数中の placeholder キーワード引数

指定された翻訳テキストは request.LANGUAGE_CODE 情報を元に取り出され、シリアライザークラスをオブジェクト化する際に適応されます。

また、locale情報に対応するキーが存在しない場合は default キーにフォールバックするため、必ず指定する必要があります。

警告

locale情報の取得にはdjangoが提供するLocaleMiddlewareを利用する必要があります。 settings.pyの MIDDLEWAREdjango.middleware.locale.LocaleMiddleware を追加してください。 詳しくは Translation を参照してください

以下に翻訳テキストを含めたシリアライザーの定義例を示します。

main:
  name: I18NTest
  fields:
  - name: animal_field
    field: ChoiceField
    field_args:
    - - -
        - default: '------- Please choice me -------'
          ja: '------- 選択してください -------'
      - - dog
        - default: Dog 🐶
          ja: イヌ 🐶
      - - cat
        - default: Cat 😺
          ja: ネコ 😺
      - - rabbit
        - default: Rabbit 🐰
          ja: ウサギ 🐰
    field_kwargs:
      help_text:
        default: Please select one of your favorite animal 😁
        ja: 好きな動物を選んでね 😁
      label:
        default: Favorite Animal
        ja: 好きな動物
  - name: introduction_field
    field: CharField
    field_kwargs:
      help_text:
        default: Please input your introduction.
        ja: 自己紹介を入力してください
      label:
        default: introduction
        ja: 自己紹介
      style:
        base_templaet: textarea.html
        rows: 5
        placeholder:
          default: Lorem ipsum dolor sit amet, consectetur...
          ja: あのイーハトーヴォのすきとおった風、夏でも底に冷たさをもつ...

Browsable APIの画面を確認すると以下の様に翻訳が適応された状態で表示されます。

_images/transfer_ja.png

ユーザーのlocaleがjaの場合

_images/transfer_fallback.png

ユーザーのlocaleが存在しなかった場合

ヒント

国際化が正しく行われたかを確認するためには、ブラウザーの Accept-Language を変更する必要があります。 もし、あなたがChromeを利用している場合は Quick Language Switcher の利用をおすすめします。


validateメソッドを含んだシリアライザー

シリアライザーやシリアライザーフィールドにはカスタムされたvalidate用のメソッドを必要とする場合があります。 これらはPythonのコードを記述する必要があります。

definable-serializerではフィールド、シリアライザーともにvalidateメソッドを記述することできます。

警告

シリアライザーの定義中にvalidateメソッドを記述できることは便利な半面、危険を伴います。 例えば、シリアライザーの定義を一般のユーザーに記述させたい要望があったとします。 もし、一般ユーザーがvalidateメソッド中にシンタックスエラーを含む内容を記述してしまうと、エラーでクラッシュしてしまいます。

また悪意のあるコードを書かれてしまうと、素直にそれが実行されてしまいます!

もし、一般ユーザーにシリアライザーを記述させたい場合は、 allow_validate_method オプションを False にして validateメソッドの記述を不許可にすることを強く推奨します。 詳しくは以下を御覧ください。

フィールドのvalidateメソッド

フィールドにvalidateメソッドを追加するには以下のように記述します。

main:
  name: FieldValidationTestSerializer
  fields:
    - name: test_field_one
      field: CharField
      field_kwargs:
        required: true

      # Field validation method
      validate_method: |
        def validate_method(self, value):
            from rest_framework import serializers
            if value != "correct_data":
                raise serializers.ValidationError("Please input 'correct_data'")
            return value

以下にバリデーションの結果を示します。

>>> from definable_serializer.serializers import build_serializer_by_yaml
>>> YAML_DEFINE_DATA = """<< FieldValidationTestSerializer YAML DATA >>"""
>>> serializer_class = build_serializer_by_yaml(YAML_DEFINE_DATA)
>>> serializer = serializer_class(data={"test_field_one": "test"})

# フィールドバリデーションエラー例
>>> serializer.is_valid()
False
>>> serializer.errors
ReturnDict([('test_field_one', ["Please input 'correct_data'"])])

# フィールドバリデーション成功例
>>> serializer = serializer_class(data={"test_field_one": "correct_data"})
>>> serializer.is_valid()
True

シリアライザーのvalidateメソッド

パスワードの確認フィールドのように、他のフィールドの入力データを利用する場合はシリアライザーのvalidateメソッドを記述する必要があります。

そのような必要がある場合は以下のように記述します。

main:
  name: PasswordTestSerializer
  fields:
  - name: password
    field: CharField
    field_kwargs:
      required: true
  - name: password_confirm
    field: CharField
    field_kwargs:
      required: true

  # Serializer  validation method
  serializer_validate_method: |-
    def validate_method(self, data):
        from rest_framework import serializers

        if data["password"] != data["password_confirm"]:
            raise serializers.ValidationError({
                "password_confirm": "The two password fields didn't match.'."
            })
        return data

以下にバリデーションの結果を示します。

>>> from definable_serializer.serializers import build_serializer_by_yaml
>>> YAML_DEFINE_DATA = """<< PasswordTestSerializer YAML DATA >>"""
>>> serializer_class = build_serializer_by_yaml(YAML_DEFINE_DATA)

# バリデーションエラー例
>>> serializer = serializer_class(
...    data={"password": "new_password", "password_confirm": "foobar"})
...
>>> serializer.is_valid()
False

>>> serializer.errors
ReturnDict([('password_confirm',
             ["The two password fields didn't match."])])

# バリデーション成功例
>>> serializer = serializer_class(
...     data={"password": "new_password", "password_confirm": "new_password"})
...
>>> serializer.is_valid()
True

Validatorクラスの利用

0.1.11で登場しました。

validateメソッドはPythonのコードを記述して入力内容のバリデーションを行うことができるため、非常に強力です。 その反面、恐ろしい事態を引き起こす要因でもあります。 また、validateメソッドは使いまわすことができないため、同じルーチンでバリデーションを行いたいフィールドが複数あると、validateメソッドを何度も記述するハメになります。 これは苦行に他なりません。

この問題を解決するには Validator クラスを利用します。

Validatorクラスを利用する場合は、各フィールドの validators のリスト中に辞書形式で以下の様に記述します(複数指定可能)。

validators:
  - validator: <パッケージ名>.<モジュール名>.<クラス名>
    args:
      ...
    kwargs:
      ...
  - validator: <パッケージ名>.<モジュール名>.<クラス名>
    args:
      ...
    kwargs:
      ...

利用するValidatorクラスは <パッケージ名>.<モジュール名>.<クラス名> の形式で指定します。 引数が必要な場合は args または kwargs を渡すことができます。

以下にテスト用のValidatorクラスを利用した記述例とバリデーション後の結果を示します。

main:
  name: UsingValidator
  fields:
  - name: hello_only_field
    field: CharField
    field_kwargs:
      style:
        placeholder: "..."
    validators:
      - validator: definable_serializer.tests.test_serializers.CorrectDataValidator
        args:
          - hello
  - name: goodbye_only_field
    field: CharField
    field_kwargs:
      style:
        placeholder: "..."
    validators:
      - validator: definable_serializer.tests.test_serializers.CorrectDataValidator
        args:
        - goodbye
_images/using_validators.png

Validatorクラスを利用した例

警告

CorrectDataValidatorクラスはテスト用です。テスト以外では利用しないように注意してください。


カスタムされた基底クラスまたはミックスインクラスの指定

0.1.14で登場しました。

Python のクラス機構はオブジェクト指向プログラミングの標準的な機能を全て提供しています。 クラスの継承メカニズムは、複数の基底クラスを持つことができ、派生クラスで基底クラスの任意のメソッドをオーバライドすることができます。

definable_serializerでは、作成されるシリアライザークラスに対して基底クラス、もしくはミックスインクラスを指定するいくつかの方法を提供しています。

django.settingsで指定する

以下のように基底クラス、またはミックスインクラスを文字列で複数指定することができます。

DEFINABLE_SERIALIZER_SETTINGS = {
    "BASE_CLASSES": [
        "foo.bar.MixinClassOne",
        "foo.bar.MixinClassTwo",
        ...
    ]
}

以降、definable_serializerで作成されるクラスは、上記で指定したクラスを継承します。

build_serializerの引数を指定する

build_serializer関数を通してシリアライザークラスを作成する場合は、base_classes 引数に基底クラス、またはミックスインクラスを指定します。

>>> from definable_serializer.serializers import build_serializer
>>> class MixinClassOne:
...     it_is_one = True
...
>>> class MixinClassTwo:
...     it_is_two = True
...
>>> serializer_class = build_serializer(
...     defn_data
...     base_classes=[
...         MixinClassOne,
...         MixinClassTwo,
...     ],
... )
...
>>> serializer = serializer_class()
>>> getattr(serializer, "it_is_one")
True
>>> getattr(serializer, "it_is_two")
True

base_classesの引数は以下のユーティリティ関数でも指定することができます

提供するモデルフィールドクラス

definable-serializerではシリアライザーを記述するためのフィールドと、 ユーザーからの入力データを保存するためのフィールドを提供しています。

シリアライザーの定義を保存するモデルフィールド

definable-serializerでは、JSON/YAMLで記述された文字列及びファイルからシリアライザーを作ることができます。 特にadminサイトでシリアライザーの定義を記述することで、デプロイの手間を無くすのが目的です。 adminサイト上でテキストデータの編集を行うのは難しい話ではないものの、YAMLやJSONをコードハイライト無しで記述するのはちょっとした苦行です。

この問題を解決するために、CodeMirror2ウィジェットを組み込んだシリアライザー定義用のフィールドを用意しています。

DefinableSerializerByYAMLField

class DefinableSerializerByYAMLField(*args, allow_validate_method=True, **kwargs)

DefinableSerializerByYAMLFieldは https://github.com/datadesk/django-yamlfield が 提供するYAMLFieldをラップし、CodeMirror2ウィジェットの利用及び非ASCII文字が正しく表示できるようにカスタマイズしています。

allow_validate_methodFalse の場合、シリアライザーの定義中に validate_method が記述されていると ValidationError が発生します。

その他のオプションについては https://github.com/datadesk/django-yamlfield を参照してください。

以下に記述例を示します。

class Survey(models.Model):
    ..

    question = DefinableSerializerByYAMLField()
_images/codemirror2_with_yaml.png

DefinableSerializerByJSONField

class DefinableSerializerByJSONField(*args, allow_validate_method=True, **kwargs)

DefinableSerializerByJSONFieldは https://github.com/dmkoch/django-jsonfield が 提供するJSONFieldをラップし、CodeMirror2ウィジェットの利用及び非ASCII文字が正しく表示できるようにカスタマイズしています。

allow_validate_methodFalse の場合、シリアライザーの定義中に validate_method が記述されていると ValidationError が発生します。

その他のオプションについては https://github.com/dmkoch/django-jsonfield を参照してください。

以下に記述例を示します。

class Survey(models.Model):
    ..

    question = DefinableSerializerByJSONField()
_images/codemirror2_with_json.png

ユーザーからの入力データを保存するモデルフィールド

入力された内容を保存する方法 でも取り上げたように、モデルに結びつかないシリアライザーの持つユーザーからの入力データを永続的に保存するには、 保存を担うモデルクラスのフィールドにシリアライズ(直列化)された状態でデータを保存します。

ようはPythonのネイティブなデータをテキストやバイナリに変換してデータベースのカラム、即ちモデルフィールドに保存できればどんな形でも構いません。

definable-serializerではユーザーからの入力を保存するために2つのモデルフィールドを用意しています。

JSONField

class JSONField(*args, **kwargs)

JSONは人気の高いシリアライズの形式です。しかし、Pythonに付属するjsonモジュールはPythonのネイティブなデータ型である set型 をシリアライズすることができません。

またensure_asciiの設定を行わないと非ASCII文字を "\uXXXX" で表してしまうため、入力情報を確認する際に見苦しい状態になります。

definable-serializerでは、 jsonfield が提供するJSONFieldをラップし、 これらの問題を解消するコンパチビリティクラスを提供しています。

以下に使用例を示します。

from definable_serializer.models.compat import JSONField as CompatJSONField

class Answer(models.Model):

    ..

    answer = CompatJSONField(
        verbose_name="answer data",
        help_text="answer data"
    )

このモデルフィールドを使うとadmin画面で以下のように表示されます。

_images/compat_json_field.png

非ASCII文字列が正しく表示されます

YAMLField

class YAMLField(*args, **kwargs)

YAMLはJSONと同様、テキストでデータをシリアライズします。記号が少なくインデントでデータ構造を表すため、Pythonのコードのように可読性に優れます。

definable-serializerでは、 django-yamlfield (https://github.com/datadesk/django-yamlfield) が提供するYAMLFieldをラップし、非ASCII文字が正しく表示されるコンパチビリティクラスを提供しています。

以下に使用例を示します。

from definable_serializer.models.compat import YAMLField as CompatYAMLField

class Answer(models.Model):

    ..

    answer = CompatYAMLField(
        verbose_name="answer data",
        help_text="answer data"
    )
_images/compat_yaml_field.png

非ASCII文字列が正しく表示されます

提供するモデルクラス

definable-serializerでは、 シリアライザーの定義を保存するモデルフィールド で紹介したシリアライザーの定義を記述するためのモデルフィールドを提供しています。

このシリアライザー定義用フィールドからシリアライザークラスを取り出すには、2つの方法があります。

1つは定義用フィールドからYAML/JSON文字列を取り出し build_serializer_by_yaml関数build_serializer_by_json関数 に渡す方法です。 もう1つは AbstractDefinitiveSerializerModel を継承したモデルクラスを用意する方法です。

ここでは、AbstractDefinitiveSerializerModel が提供する機能について説明します。

AbstractDefinitiveSerializerModel

class AbstractDefinitiveSerializerModel(*args, **kwargs)

このモデルクラスは django.db.models.Model を親クラスとしており、 __init__ メソッドの中で DefinableSerializerByYAMLField および DefinableSerializerByJSONField を利用しているフィールドを自動で探し、 get_<フィールド名>_serializer_class というメソッドをモデルオブジェクトに付与します。

モデルクラスは複数のシリアライザー定義用のフィールドを持つことも可能です。 以下のようなモデルクラスを定義した場合、 get_foo_serializer_classget_bar_serializer_class という2つのメソッドが自動でモデルオブジェクトに付与されます。

from definable_serializer.models import (
    DefinableSerializerByYAMLField,
    DefinableSerializerByJSONField,
    AbstractDefinitiveSerializerModel,
)

class MyModel(AbstractDefinitiveSerializerModel):
    ...

    foo = DefinableSerializerByYAMLField()
    bar = DefinableSerializerByJSONField()
>>> my_model = MyModel.objects.get(pk=1)
>>> my_model.get_foo_serializer_class
function
>>> my_model.get_bar_serializer_class
function
>>> my_model.get_foo_serializer_class()
NameEntry():
    first_name = CharField(max_length=100, required=True)
    last_name = CharField(max_length=100, required=True)

>>> my_model.get_bar_serializer_class()
Group():
    group_name = CharField(label='Group name', required=True)
    persons = Person(many=True):
        first_name = CharField(required=True)
        last_name = CharField(required=True)

提供するモデルアドミンクラス

JSONやYAMLで定義したシリアライザーは、実際にシリアライザークラスにしないと確認することができません。 YAMLで記述された定義からシリアライザーを作成する で説明した方法で確認することもできますが、入力や見栄えのテストを行うのには不十分です。 そのため、definable-serializerでは django.contrib.admin.ModelAdmin を拡張した DefinableSerializerAdmin クラスを提供しています。

DefinableSerializerAdmin

class DefinableSerializerAdmin

DefinableSerializerAdminクラスでは以下の機能を提供します。

  • 編集画面上部に定義されたシリアライザークラスの情報を表示する機能
  • restframeworkのもつBrowsable APIページを利用して定義されたシリアライザーを表示する機能

特にBrowsable APIページでは定義したシリアライザーの入力テストを行うことができるため、 非常に有用です。組み込み方も非常に簡単で、通常は admin.ModelAdmin を利用するところを DefinableSerializerAdmin に入れ替えるだけです。

以下に使用例を示します。

from django.contrib import admin
from definable_serializer.admin import DefinableSerializerAdmin

from . import models as surveys_models


@admin.register(surveys_models.Survey)
class SurveyAdmin(DefinableSerializerAdmin):

    list_display = (
        "id",
        "title",
    )

    list_display_links = (
        "id",
        "title",
    )

正しく組み込まれるとadmin画面が以下のようになります。

_images/using-extend-admin.png

画面上部にシリアライアーの定義が表示されます。

提供するシリアライザーフィールドクラス

Zen Of Pythonの 暗示するより明示するほうがいい という観点からdefinable-serializerでは TemplateHTMLRenderer のためにいくつかのフィールドを提供しています。

警告

これらのフィールドは将来的に別パッケージとして提供される可能性があります。


CheckRequiredField

class CheckRequiredField(*args, **kwargs)

必ずOnをしなければならないチェックボックスを提供します。ユーザーの意思確認などを行いたい場合に利用します。

このクラスは restframeworkの BooleanField を継承してつくられています。 オプションについては BooleanField を参照してください。

シリアライザー定義のfieldには definable_serializer.extra_fields.CheckRequiredField を指定します。

main:
  name: Agreement
  fields:
  - name: agreement
    field: definable_serializer.extra_fields.CheckRequiredField

MultipleCheckboxField

class MultipleCheckboxField(choices, *args, required=False, inline=False, **kwargs)

複数のチェックボックスによる選択肢を表示するフィールドを提供します。

fieldには definable_serializer.extra_fields.MultipleCheckboxField を指定します。

  • requiredtrue にすると必須選択になります。
  • inlinetrue にするとチェックボックスが横並びに表示されます。

このクラスはrestframeworkの MultipleChoiceField を継承してつくられています。その他のオプションについては MultipleChoiceField を参照してください。

main:
  name: YourFavoriteAnimal
  fields:
  - name: animal_choice_field
    field: definable_serializer.extra_fields.MultipleCheckboxField
    field_args:
    - - - dog
        - 🐶Dog
      - - cat
        - 😺Cat
      - - rabbit
        - 🐰Rabbit
    field_kwargs:
      inline: true
      required: true
      label: Lovely Animals
      help_text: Please choice your favorite animal
_images/multiple_animal_choice.png

インライン化されたMultipleCheckboxField


ChoiceRequiredField

class ChoiceRequiredField(choices, *args, **kwargs)

0.1.12 で登場しました。

選択必須のリストを提供します。

基本的な動作は ChoiceField と変わりませんがユーザーに選択を促すブランクチョイスを入れるため、 choices の1つ目の値が必ずnull値である必要があります。

このクラスは restframeworkの ChoiceField を継承してつくられています。その他のオプションについては ChoiceField を参照してください。

main:
  name: YourFavoriteAnimal
  fields:
  - name: animal_choice_field
    field: definable_serializer.extra_fields.ChoiceWithBlankField
    field_args:
    - - - null
        - "-------- Please Choice one 😉 --------"
      - - dog
        - 🐶Dog
      - - cat
        - 😺Cat
      - - rabbit
        - 🐰Rabbit
    field_kwargs:
      label: Lovely Animals
      blank_label: '-------- Please Choice 😉 --------'
      help_text: Please choice your favorite animal

ChoiceWithBlankField

警告

ChoiceWithBlankFieldクラスは廃止予定です。変わりに ChoiceRequiredField を利用してください。

class MultipleCheckboxField(choices, *args, blank_label=None, **kwargs)

渡されたchoicesの選択にブランクチョイスを自動的に追加します。ブランクチョイスが選択された状態でバリデーションが 行われるとエラーになります。

fieldには definable_serializer.extra_fields.ChoiceWithBlankField を指定します。

  • blank_label に文字列を渡すとダッシュの連続の代わりにその文字列がブランクチョイスの部分に表示されます。

このクラスは restframeworkの ChoiceField を継承してつくられています。その他のオプションについては ChoiceField を参照してください。

main:
  name: YourFavoriteAnimal
  fields:
  - name: animal_choice_field
    field: definable_serializer.extra_fields.ChoiceWithBlankField
    field_args:
    - - - dog
        - 🐶Dog
      - - cat
        - 😺Cat
      - - rabbit
        - 🐰Rabbit
    field_kwargs:
      label: Lovely Animals
      blank_label: '-------- Please Choice 😉 --------'
      help_text: Please choice your favorite animal
_images/choice_with_blank_field.png

blank_labelに文字を渡した例。blank_labelが空の場合は "---------" となります。


RadioField

class RadioField(choices, *args, inline=False, **kwargs)

ラジオボタンによる選択肢を表示するフィールドを提供します。

fieldには definable_serializer.extra_fields.RadioField を指定します。

  • inlinetrue にするとチェックボックスが横並びに表示されます。

このクラスは restframeworkの ChoiceField を継承してつくられています。その他のオプションについては ChoiceField を参照してください。

main:
  name: YourFavoriteAnimal
  fields:
  - name: animal_choice_field
    field: definable_serializer.extra_fields.RadioField
    field_args:
    - - - dog
        - 🐶Dog
      - - cat
        - 😺Cat
      - - rabbit
        - 🐰Rabbit
    field_kwargs:
      inline: true
      required: true
_images/radio_field.png

インライン化されたRadioField


TextField

警告

TextFieldクラスは廃止予定です。変わりに CharField を利用してstyle引数を渡してください。 詳しくは field-styles を参照してください。

また、placeholder引数は フィールドの国際化 翻訳の対象になりません。

テキストエリアを提供します。

fieldには definable_serializer.extra_fields.TextField を指定します。

  • rows に数値を渡すことででテキストエリアの行数を指定することができます。
  • placeholder に文字列を渡すとプレースホルダー文字列を表示することができます。

このクラスは restframeworkの CharField を継承してつくられています。その他のオプションについては CharField を参照してください。

_images/text_field.png

placeholderとrowsを設定した例

提供する関数

definable-serializerでは、YAMLやJSONで記述された定義からシリアライザーを作成する5つの関数を提供しています。

  • build_serializer
  • build_serializer_by_json
  • build_serializer_by_json_file
  • build_serializer_by_yaml
  • build_serializer_by_yaml_file

build_serializer関数

build_serializer(definition, base_classes=[], allow_validate_method=True)

build_serializer は定義が記述されたPythonのDictからシリアライザークラスを作成します。

base_classes 継承するクラスを指定することができます。

allow_validate_methodFalse の場合、シリアライザーの定義中に validate_method が記述されていると ValidationError が発生します。

>>> from definable_serializer.serializers import build_serializer
>>> serializer_definition = {
...     "main": {
...         "name": "TestSerializer",
...         "fields": [
...             {
...                 "name": "test_field",
...                 "field": "CharField",
...                 "field_kwargs": {
...                     "max_length": 100,
...                 }
...             }
...         ],
...         "validate_method": """def validate_method(self, value):
...             return value
...         """
...     },
... }
# allow_validate_methodがTrueの場合
>>> serializer_class = build_serializer(serializer_definition, allow_validate_method=True)
>>> serializer_class()
TestSerializer():
    test_field = CharField(max_length=100)

# allow_validate_methodがFalseの場合
>>> serializer_class = build_serializer(serializer_definition, allow_validate_method=False)
ValidationError: ['serializer validate_method not allowed.']

build_serializer_by_json関数

build_serializer_by_json(definition, base_classes=[], allow_validate_method=True)

build_serializer_by_json は定義が記述されたJSON文字列からシリアライザークラスを作成します。

base_classes 継承するクラスを指定することができます。

allow_validate_methodFalse の場合、シリアライザーの定義中に validate_method が記述されていると ValidationError が発生します。

>>> from definable_serializer.serializers import build_serializer_by_json
>>> json_str = """
... {
...     "main": {
...         "name": "TestSerializer",
...         "fields": [
...             {
...                 "name": "test_field",
...                 "field": "CharField",
...                 "field_kwargs": {"max_length": 100}
...             }
...         ],
...         "validate_method": "def validate_method(self, value):\\n            return value\\n        "
...     }
... }
... """

# allow_validate_methodがTrueの場合
>>> serializer_class = build_serializer_by_json(json_str, allow_validate_method=True)
>>> serializer_class()
TestSerializer():
    test_field = CharField(max_length=100)

# allow_validate_methodがFalseの場合
>>> serializer_class = build_serializer_by_json(json_str, allow_validate_method=False)
ValidationError: ['serializer validate_method not allowed.']

build_serializer_by_json_file関数

build_serializer_by_json_file(json_filepath, base_classes=[], allow_validate_method=True)

build_serializer_by_json_file は定義が記載されたJSONファイルからシリアライザークラスを作成します。

この関数の動作はファイルパスを受け取る以外、 build_serializer_by_json 関数と同等です。


build_serializer_by_yaml関数

build_serializer_by_yaml(definition, base_classes=[], allow_validate_method=True)

build_serializer_by_json 定義が記述されたYAML文字列からシリアライザークラスを作成します。

base_classes 継承するクラスを指定することができます。

allow_validate_methodFalse の場合、シリアライザーの定義中に validate_method が記述されていると ValidationErrorが発生します。

>>> from definable_serializer.serializers import build_serializer_by_yaml
>>> yaml_str = """
... main:
...   name: "TestSerializer"
...   fields:
...   - name: test_field
...     field: CharField
...     field_kwargs:
...       max_length: 100
...   validate_method: |
...   def validate_method(self, value):
...       return value
... """

# allow_validate_methodがTrueの場合
>>> serializer_class = build_serializer_by_yaml(yaml_str, allow_validate_method=True)
>>> serializer_class()
TestSerializer():
    test_field = CharField(max_length=100)

# allow_validate_methodがFalseの場合
>>> serializer_class = build_serializer_by_yaml(yaml_str, allow_validate_method=False)
ValidationError: ['serializer validate_method not allowed.']

build_serializer_by_yaml_file関数

build_serializer_by_yaml_file(yaml_filepath, base_classes=[], allow_validate_method=True)

build_serializer_by_yaml_file 定義が記載されたYAMLファイルからシリアライザークラスを作成します。

この関数の動作はファイルパスを受け取る以外、 build_serializer_by_yaml 関数と同等です。

その他の有用な情報

ここではいくつかの有用な情報及び注意点などを記載します。

definable-serializer-exampleについて

プロジェクトにdefinable-serializerを組み込む で作成したサンプルプロジェクトになります。 実際に動作するコードを参照したい場合はこちらを御覧ください。

https://github.com/salexkidd/restframework-definable-serializer-example


definable-serializerのつかいどころ

入力フィールドの変更が多いシリアライザーから可哀想なモデルを解放するのがdefinable-serializerの役割です。 逆にフィールドの変更が少ないシリアライザーはモデルシリアライザーを用いて作成するべきです。

柔軟性はシステムに変更のチャンスを与えますが、過ぎたる柔軟性は土台を揺るがしシステムの崩壊時期を早めます。

definable-serializerが向いているのは、入力内容が場合によって変更されるものの1つのモデルクラスで取り扱いたい場合や、モデルのメタ情報を扱う場合、そしてモックを作る場合です。

それ以外での利用は十分に注意し、考えた末で利用を検討するべきです。


JSONFieldでデータを扱う場合の問題点

サードパッケージ及びdjangoが提供するJSONFieldは有用なモデルフィールドであるものの、利用するには2つの問題があります。

1つはJSONEncoderの問題、もうひとつが非ASCII文字列の問題です。

そのためにdefinable-serializerではいくつかのフィールドを提供しています(詳しくは 提供するモデルフィールドクラス を参照してください)。

しかし、場合によっては提供するモデルフィールドを利用できないケースもあります。

特に、postgreSQLを利用しておりユーザーからの入力データを検索対象にしたいのならば、djangoが提供する django.contrib.postgres.fields.JSONField 利用するのがベストです。

もし、MySQLを利用しているのならば django-mysql を利用するとよいでしょう。

ただし、これらのJSONFieldにはJSONEncoder、及び非ASCII文字に関する問題があります。

もし、definable-serializerが提供する以外のJSONFieldを利用するにはこれらの問題に対処する必要があります。

JSONEncoder問題

PythonでデータをJSONにシリアライズする場合、大きな落とし穴があります。 それは、ネイティブデータ型である set型 がpythonに付属するjsonモジュールではエンコードすることができないからです。

set型 を含むデータをjson化すると`` TypeError`` が発生するのを確認できます。

>>> import json
>>> json.dumps(set([1,2,3]))
TypeError: Object of type 'set' is not JSON serializable

困ったことに、restframeworkが提供するMultipleChoiceフィールドはバリデーション後の結果を set型 で返すため、そのままJSONFieldに値を渡すとエラーが発生してしまいます。

この問題を解決するには DjangoJSONEncoder を継承して自作のEncoderクラス用意し、 set型 のデータを list型 に変換するように変更します。

from django.db import models
from django.core.serializers.json import DjangoJSONEncoder
from django.contrib.postgres.fields import JSONField


class MyCustomJSONEncoder(DjangoJSONEncoder):
    def default(self, o):
        if isinstance(o, set):
            return list(o)
        else:
            return super().default(o)


class TestModel(models.Model):
    answer = JSONField(encoder=MyCustomJSONEncoder)

非ASCII文字列問題

以下のJSON文字列を見てみましょう

{"favorite_food": "\ud83c\udf54"}

これは、ハンバーガー(🍔)のEmojiです。しかし、'\ud83c\udf54' は全く美味しそうに見えません。 目に見る必要がないデータならばこれで問題ありませんが、adminサイトで入力されたデータを確認しようとして、"\ud83c\udf54" のような文字列が表示されたらどうでしょうか。

エンジニアならばこの文字列をデコードして意味を知ることができるかもしれません。 しかし、実際にデータを扱うオペレーターから見ると不吉な何かにしか見えないでしょう。

_images/bad_taste_burger.png

ハンバーガー的な何か

この問題を避けるには、eusure_ascii オプションを False にしてdumpを行う必要があります。 以下にコード例を示します。

>>> import json
>>> input_data = {
...     "favorite_food": "🍔"
... }
>>> json.dumps(input_data)
'{"favorite_food": "\\ud83c\\udf54"}'
>>> json.dumps(input_data, ensure_ascii=False)
'{"favorite_food": "🍔"}'

ensure_asciiFalse にしたい場合、モデルフィールドのソースコードを読み、各自で json.dumps の部分を変更してオプションを渡すようにしなければなりません。

JSONFieldの供給過多問題

JSONFieldにはもう1つ問題があります。世界中のエンジニアはJSONを好んで利用します。 その結果、Googleで調べるといくつものJSONFieldがdjangoに提供されていることが確認できます。

また、djangoも django.contrib.postgres.fields.JSONField を提供しています。

ハッキリ言えば供給が多すぎて、どれを利用してよいか迷ってしまいます。

きっと優秀なあなたならば間違えないでしょう。しかし、筆者はpipでインストールを行う際に十中八九間違えます。 (余談ながら、上記パッケージの大半が JSONEncoder問題 及び 非ASCII文字列問題 を抱えています。)

これらの問題に一番対処しやすいのが django-jsonfield (上記リストの先頭)です。

フィールドの引数に対して `dump_kwargs を渡すことで、JSONEncoder及びensucre_ascii問題に対処することができます。

definable-serializerでは、 DefinableSerializerByJSONField および JSONField においてdjango-jsonfieldを利用しています。



Todo

TodoはGithub上で管理しています。


連絡先

twitter: @salexkidd

ライセンス

Copyright 2017 salexkidd

Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions:

The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.

THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.

Indices and tables