Djangoでgettext_lazyをJSON Keyとして使うと頭を抱える状況

結論を先に一言でまとめると:JSON Keyは必ず「本物の」文字列でなければならない。しかし、Djangoのgettext_lazyが返すのは、実際には文字列ではなく__proxy__というオブジェクトです。

1. 順調に動いていたコードがなぜ突然?

Django開発において、多言語対応のためにgettext_lazyを使うのは日常的なことです。ところが、普段通りに使っていたのに、ある日突然JSONレスポンス(JsonResponse)の過程で直列化(Serialization)エラーが発生することがあります。

通常、エラーログを見ても__proxy__といったような、意味不明なメッセージしか表示されないため、戸惑うものです。しかし、原因を紐解いてみると、意外にも単純なところに犯人がいます。

gettext_lazyとJSON Keyの問題に関するインフォグラフィック

2. 犯人は「Key」にあった

結論から言うと、gettext_lazyオブジェクトは辞書のValueとして使う場合は問題なく処理されますが、Keyとして使った瞬間に問題を引き起こします。

区分 Valueで使った場合 Keyで使った場合
動作可否 正常(自動でstrに変換される) エラー発生
理由 JSON直列化時に自動型変換をサポート JSON Keyは必ず「本物の」文字列でなければならない

3. なぜこのような違いが生じるのか?

gettext_lazyが返すのは、実際のStringではなく__proxy__というオブジェクトです。その名の通り、「後で必要になったら翻訳して渡すよ」と約束だけしている状態です。

Pythonのjson.dumps()やDjangoのJsonResponseは、辞書内の値を走査しながら、この「約束」を自動的に文字列に展開(評価)してくれます。しかし、辞書のKeyとなると話は別です。JSONの標準ではKeyは必ず文字列でなければなりませんが、この過程で__proxy__オブジェクトが自動的に変換されず、そのまま衝突してしまうためエラーが発生するのです。

# ✅ 問題なし: Valueは自動的にstrに変換されます。
json.dumps({'language': _('Korean')})

# ❌ エラー: Keyは自動的に変換されず、拒否されます。
json.dumps({_('Korean'): 'language'})

4. どのように解決するか?

方法1: 単純にgettextを使う(最もシンプル)

Lazy方式がどうしても必要な状況でなければ、呼び出しと同時に文字列を返すgettextを使うのが最も手軽です。

from django.utils.translation import gettext as _ 

LANGUAGE_MAP = {
    "en": _("English"),
    "ko": _("Korean"),
}
  • 参考: ただし、この方式はアプリがロードされる時点で翻訳が決定されるため、動的な翻訳環境が重要である場合は注意が必要です。

方法2: 直列化直前にstr()を挟む

すでにLazyオブジェクトを広範囲で使っている場合は、JSONに渡す直前に強制的に文字列変換を行います。

# Keyとして利用する前にstr()で一度囲み、「本物の文字列」にします。
lang_key = str(LANGUAGE_MAP.get(code))

方法3: 翻訳の主体をクライアントに任せる

もしフロントエンドでDjangoのテンプレートを使わず、別のフロントエンドクライアントを使用する方式であれば(最近ではこの方式がトレンドかもしれませんが)、Django(あるいはDRF)サーバーはenkoのようなコード(Code)だけを返し、実際に画面に表示するテキストはフロントエンド(React、Vueなど)のi18nライブラリで処理する方式です。サーバーロジックが軽量化されるという利点があります。


まとめ

json.dumps()gettext_lazy()を組み合わせて使用していて、「昨日は動いたのに、なぜ今日は動かないんだろう?」という状況に遭遇したなら、おそらく昨日は運良くValueにのみ使用し、今日はKeyに誤って使用した可能性が高いです。

gettext_lazyは便利ですが、結局のところ「本物の文字列」ではないという点を常に念頭に置いておく必要があります。特にJSONを扱う際にはなおさらです。同様のエラーで苦労されている方々の一助となれば幸いです。