Overview

infographic about gettext_lazy and JSON key problem

While developing with Django, there are many occasions where we use gettext_lazy for multilingual support. However, there are times when gettext_lazy, which we usually rely on, suddenly causes errors when creating JSON responses.

This article explains why gettext_lazy causes issues in JSON serialization and how to resolve them in detail.

Normal Operation vs Error Occurrence: Situation Comparison

Case Normal Operation Error Occurred
LANGUAGE_MAP.get(...) Return Value Location Value of the dictionary Key of the dictionary
Serialization Feasibility Possible (No issues) Not possible (__proxy__ key cannot be converted to JSON)

Core of the Problem: Why Does This Difference Exist?

The object returned by gettext_lazy is of type __proxy__. This object looks like a string but is not an actual string.

Django's JsonResponse and Python's json.dumps() follow the next rules:

# ✅ Possible: lazy objects can be used as values (automatically converts to str)
json.dumps({'language': _('English')})

# ❌ Failure: lazy objects cannot be used as keys
json.dumps({_('English'): 'language'})

This means that to be used as a dictionary key, it must be a real string. The __proxy__ object does not automatically convert when used as a key, leading to an error.

Why Is There No Problem with Value but Issues with Key?

The gettext_lazy object does not pose issues when used as a value in a dictionary. This is because Django or Python automatically converts the value to a string during JSON serialization.

However, when used as a key, the JSON standard enforces that keys must be strings. In this case, the gettext_lazy object does not convert automatically, resulting in a serialization error.

Conclusion:

  • There are no problems when used as a value because it is internally converted to str.
  • When used as a key, conversion does not happen automatically, causing errors.

Solutions

Method 1: Switch to gettext (Not using Lazy)

This is the most straightforward solution. Use values that are converted directly to strings without using lazy.

from django.utils.translation import gettext as _  # not lazy!

LANGUAGE_MAP = {
    "en": _("English"),
    "ko": _("Korean"),
    "ja": _("Japanese"),
}
  • Advantage: No additional processing needed
  • Disadvantage: Translations are determined at the initial import time, which may not meet some dynamic translation requirements

Method 2: Force str() Conversion Just Before JSON Serialization

If you want to continue using lazy objects, convert the key using str() before placing it in JSON.

lang = str(LANGUAGE_MAP.get(lang_code, lang_code.upper()))
  • Advantage: Minimal changes to existing code
  • Disadvantage: Must pay attention to str conversion every time

Bonus: Handle Translation on Client Side

Another method is to not perform multilingual translations on the server and instead handle them on the frontend.

  • The server only sends the language code
  • Client-side JavaScript manages the translation table.

This method can be chosen depending on the project.

In Summary

  • Never use gettext_lazy as a dictionary key
  • Always convert to string before JSON serialization
  • The safest method is immediate string conversion using gettext
  • If retaining existing code, don't forget to use str(...)

Comments from Jesse

This issue is a common mistake that occurs at the very subtle boundary between multilingual handling in Django and JSON response processing.

Instead of asking, "It used to work, why doesn't it now?", consider that "It was just luck before" and review your code.

The moment a developer's skills accumulate is when they understand such 'subtleties'. With this principle in mind, you can make both Django multilingual handling and JSON responses much more robust!