Django-NinjaでSchemaからValidationErrorをハンドリング

# 前置き
最近会社でAPIを作っています。
フレームワークにDjango-Ninjaを選定しました。
理由はシゴデキの先輩&別チームが使ってたからです。
FastAPI（使ったことない）っぽく記述ができてPydantic（使ったことない）っていうのと密接なんだ～くらいのところからスタート。

# やりたいこと
リクエストを貰った際にSchemaがパラメータのバリデーションを勝手にしてくれるんですが、そこで発生するValidationErrorをいい感じにハンドリングしたいです。
ただ、どうもDjango-Ninjaの情報があまりネットに落ちてなくて、日本語の情報も全然ないし、Qiitaにもほぼなくて。
というわけで完全我流。なんとかします。

**底辺が底辺なりになんとかしてみたくらいのノリです。**
**(._.)**

# 使用したバージョン・ライブラリ
あまり覚えてないのですが、PydanticはV2っていう新しい方を使ったと思います。
たしか↓です。

- Django-Ninja 1.1.0
- Pydantic-core 2.14.6

# 実装
## 方針
ひとまず公式ドキュメントを参考にして、NinjaAPIクラスのValidationErrorを上書きしてみます。
SchemaではPydanticを使って適当に条件分けとかしつつ、ValidationErrorをraiseする。

## 中身
### urls.py
APIのセットアップぽいことをつらつら書いてます。
必要なのは、デフォルトのエラーハンドラを上書きすることかなーと思います。
一旦公式の1stステップそのまんまに、views.pyとか使わないでここに色々書いていきます。
（本当は分けたほうがいいはず……。多分。）

```python
from django.http import HttpResponse
from ninja.errors import ValidationError
from ninja.renderers import BaseRenderer
from ninja import NinjaAPI


# JSONのレンダリングをするクラス
class ORJSONRenderer(BaseRenderer): 
    media_type = "application/json"

    def render(self, request, data, *, response_status):
        return orjson.dumps(
            data,
        )


# APIにレンダリングクラスをあげる
api = NinjaAPI(renderer=ORJSONRenderer())

# デフォルトのエラーハンドラをカスタマイズ
@api.exception_handler(ValidationError)
def validation_errors(request, exc):
    # Pydanticがバリデーションエラーのタイプを"missing"って言ってるとき
    if exc.errors[0]["type"] == "missing":
        return api.create_response(
            request,
            {"error_message": "パラメータが無いよ"},
            status=400,
        )
    # 適当に自分でエラーのタイプを作ったり他のPydanticのバリデーションエラーのとき
    elif exc.errors[0]["type"] in [
        "nanka_error",
        "yabai_error",
        "tekitou_ni_error_wo_tsuika_suru",
    ]:
        return api.create_response(
            request,
            {"error_message": "なんかエラー起きてるよ"},
            status=403,
        )
    # その他のバリデーションエラーが起きてるとき全部
    else:
        return api.create_response(
            request,
            {"error_message": "ヤバいエラーだよ"},
            status=500,
        )

@api.get("")
def test(request, parameters: Query[TestIn]): # Queryでリクエストのスキーマを教えてあげる
    return {
        "id": parameters.id,
        "value": parameters.value,
    }

urlpatterns = [
    path("admin/", admin.site.urls),
    path("", api.urls),
]
```

### my_schema.py
スキーマです。スキーマの意味がよくわからない。
ここではリクエストパラメータを定義してます。
GETリクエストを受け付ける想定で、クエリパラメータのバリデーションとかを設定する。

```python
from pydantic import ValidationInfo, field_validator
from ninja import Schema, Field


class TestIn(Schema):
    # クエリパラメータを2つ定義してみる。idは必須
    id: int = Field(
        description="ID dayo.",
        example=1,
    )
    value: str | None = Field(
        description="VALUE dayo.",
        default=None, # defaultをつけると必須じゃなくなる
        example="testing",
    )

    @field_validator("id")
    def validate_id(value):
        if value == 0:  # idが0ならエラーにしてみる
            raise ValidationError(
                errors=[  # errorsにPydanticのバリデーション結果が入るっぽい？
                    {
                        "type": "nanka_error",
                        "loc": [
                            "query",
                            "id",
                        ],
                        "msg": "メッセージも欲しいなら書く",
                    }  # ここがバリデーションの情報
                ]
            )
        return value

```

# 結果
バリデーションエラー時のレスポンスが、内容によって変わるようになりました。
とりあえずこれをベースにしたら割と自由気ままにできそう。うれしい。

## 必須パラメータがないとき
クエリパラメータにidが渡されていない場合はこんな感じ。
ちゃんとパラメータがないことを教えてくれる。
`http://127.0.0.1:8000/?value=testing`

```json
{"error_message":"パラメータが無いよ"}
```

## 使えない値があるとき
Schemaに独自に定義したバリデーションの部分。
idに0はダメだよっていうもの。
メッセージが優しくないけど……。
`http://127.0.0.1:8000/?id=0&value=testing`

```json
{"error_message":"なんかエラー起きてるよ"}
```

## idを空文字で渡したとき
idを空文字で渡すパターンも試してみる（偉い）。
「Schemaでidをint型にしてるのに文字列が渡されちゃってるからintにできないじゃん」て教えてくれてるのかな？
`http://127.0.0.1:8000/?id=&value=testing`

```json
{"message":"ヤバいエラーだよ"}
```

レスポンスをカスタマイズしないでエラーを起こしてみると、こんな感じ。
typeがint_parsingってなってる。
このtypeは条件分岐に書いてないから、elseで処理されてる。
```json
{"type":"int_parsing","loc":["query","id"],"msg":"Input should be a valid integer, unable to parse string as an integer"}
```


# 感想
ベストプラクティスみたいなのを盲目的にただ追いかけたり真似したりするほうが安心する性格（ISFJ）なので、我流はちょっと気持ち悪いなって思います。
「もっと良い方法あるんだろうなあ」、「なにもわかっちゃいないのにこんな偉そうなこと書いちゃって」みたいな。

久しぶりにQiitaを書いてみたけど、けっこう大変だった。
日本語書きながらコードも書くって、キーボードとか入力の設定によってはだるい。
でも必然的に調べる量が増えるので、そこはいいかも。まる。

三点リーダを2回連続でいれるとQiitaに怒られる。
2回連続が推奨だと思ってたんだけど違うのかな。

これ書いている途中で色々試してたら、Python組み込みの型の名前をレスポンスしなくちゃいけないときとかどうしようって悩みが生まれてしまいました。
listっていうキーを含んだJSONをレスポンスしたいけど、Schemaでlistっていうフィールド名は使っちゃまずいし。
aliasをFieldに設定してみたけど、思ってた挙動とは違ったし。
どうしよう。わかんない。 **誰か教えてほしい。**

そんな感じです。悩んでます。色々。

じゃあね。

# 参考
https://django-ninja.dev/guides/errors/?h=create_res

https://django-ninja.dev/guides/input/query-params/