REST Framework で unique_together の エラーメッセージを変えたい




- この記事は [Django Advent Calendar 2018](https://qiita.com/advent-calendar/2018/django) の 8日目(代打) と [BeProud Advent Calendar 2018](https://adventar.org/calendars/3338) と17日目の記事です。
- REST Framework の `ModelSerializer` を使った時に `unique_together` のエラーメッセージを変えたかった。
- 通常 `Model` に `unique_together` が設定されていたら、`ModelSerializer`はその設定従い `validation` してくれる
- この時のエラーメッセージは `Model` のデフォルトのエラーメッセージとなる
- But そのエラメッセージをもう少しわかりやすいものに変えたかったが割とめんどうだったという話

## 前提

- Python 3.7
- Django 2.1.4
- djangorestframework 3.9.0

## 確認

- まずデフォルトのエラーがどうなるか確認する。
- 適当に `unique_together` を持つ `Model` と、それを使う `ModelSerializer` を用意する

```python

# model ---

class Spam(models.Model):

   name = models.CharField('name', max_length=255)
   company_id = models.IntegerField('company id')

   class Meta:
        unique_together = ('name', 'company_id')

# serializer ---

class SpamSerializer(serializers.ModelSerializer):

    class Meta:
        model = Spam
        fields = '__all__'
```

- `unique_together` のエラーを確認

```py
>>> from spam.serializers import SpamSerializer
>>> s = SpamSerializer(data={'name': 'name1', 'company_id': 1})
>>> s.is_valid()
True
>>> s.save()
<Spam: Spam object>
>>> s = SpamSerializer(data={'name': 'name1', 'company_id': 1})
>>> s.is_valid()
False
>>> s.errors
{u'non_field_errors': [ErrorDetail(string=u'The fields name, company_id must make a unique set.', code=u'unique')]}
```

- **"The fields name, company_id must make a unique set."** の部分を適当にカスタマイズしたい


## どうするか？

- とりあえず適当にググってみた結果、以下の方法が出てきた
- https://stackoverflow.com/a/49751507

１. `Serializer` 側に別途 `UniqueTogetherValidator` を設定する

```python
UniqueTogetherValidator(message='Your custom message', fields=(field1, field2,))
```

-> エラーメッセージを変えたいだけなのに別途validation設定するのなんか微妙

２. `Model` の `unique_error_messageメソッド` を `override` する

```python
def unique_error_message(self, model_class, unique_check):
    error = super().unique_error_message(model_class, unique_check)
    # Intercept the unique_together error
    if len(unique_check) != 1:
        error.message = 'Your custom message'
    return error
```

-> これもなんか微妙にめんどい、カスタムなエラーメッセージは `Serializer側` に寄せたいキムチがある

## メッセージだけ書き換えるやつを作る


- 結局 `ModelSerializer` は `unique_together` があったら、どっかで `UniqueTogetherValidator` を使ってるんじゃろう？
- そこのメッセージだけ変えるようなものを作ればええんやんかという都合の良い推測を立てる。
- 調べて見た結果 `get_unique_together_validators` とかいうメソッドを持ってるやんかということがわかったので適当に `override` してメッセージを差し替えるようにする
- https://github.com/encode/django-rest-framework/blob/master/rest_framework/serializers.py#L1488


```python
class MyModelSerializer(ModelSerializer):

    def get_unique_together_validators(self):
        """ override ModelSerializer.get_unique_together_validators """

        validators = super().get_unique_together_validators()
        kwargs = self.get_extra_kwargs()

        my_errors = kwargs.get('my_errors', {})
        errors = my_errors.get('unique_together')
        if not my_errors or not errors:
            return validators

        for v in validators:
            if v.fields in errors:
                v.message = errors.get(v.fields)
        return validators


# overrideした Serializerをbaseにする
class SpamSerializer(MyModelSerializer):

    class Meta:
        model = Spam
        fields = '__all__'
        extra_kwargs = {
            'my_errors': { # <- 適当に自分で決めたエラメッセージを設定する
                'unique_together': {
                    ('name', 'company_id'): "その名前は既に使われてはりますなぁ(にちゃぁぁ"
                }
            }
        }
```

実行結果

```python
>>> from spam.serializers import SpamSerializer
>>> s = SpamSerializer(data={'name': 'name1', 'company_id': 1})
>>> s.is_valid()
False
>>> s.errors
{'non_field_errors': [ErrorDetail(string='その名前は既に使われてはりますなぁ(にちゃぁぁ', code='unique')]}
```

これでやりたいことはとりあえず出来た。

## まとめ

- `unique_together` の エラーメッセージ変えるだけが結構めんどい。
- メッセージ部分だけを`override` するような `Serializer` を用意して対処した。
- But このやり方は Undocumented で将来的な restframework の変更に対して何も保証がないので注意が必要。 -> **restframework の バージョンアップとともに壊れる可能性がある**
- 多分一番無難で安全なのは `UniqueTogetherValidator` をひとつづ設定していくのが良い。

以上おわり