Django
Django2.1
jsonencoding

[Django]ModelをJsonEncodeする際に日付や数値を書式設定したい


やりたいこと


  • Djangoのモデルをシリアル化(JSON)してクライアントにレスポンスしたい

  • この時に日付を任意の書式に整形したい(例えば "19/03/01 13:00")

  • 数値を書式設定および単位を付与したい(例えば 10000 を "10,000円"とか"10,000部"とか)

  • 整形はサーバサイドで実行したい


課題

Djangoモデルをシリアル化するためにDjangoが用意しているシリアライザ-(django.core.serializers.serialize / DjangoJSONEncoder)を使うと簡単にシリアル化できますが、以下の問題があります。


datetime 型が冗長な表示になる

2019-03-06T17:46:04.013Z


シリアル化対象はモデルフィールドだけ

またモデルのフィールドしか対象にしてくれません。書式設定したメソッドやプロパティを用意しても含めることができません。(このできないという結論に至るまでの調査に時間を要したのでこの記事を書きました。参考になれば幸いです)


余計な情報も含まれてしまう

またモデルのフィールドだけでなく、モデルの名称や主キー値も含まれてしまうので冗長です。(クライアント側で必要なデータだけを取り出すというDjango依存の処理が必要になってしまう)

In [11]: serializers.serialize('json',[os], stream=sys.stdout, indent=2)

[{
"model": "order.ordersummary", ★ここ★
"pk": 1, ★ここ★
"fields": {
・・・
}
}]


対処方法


案1

公式ドキュメントにある手法です。

JSONシリアライズ用のエンコーダをカスタマイズして、それをシリアライザーに渡します。

from django.core.serializers.json import DjangoJSONEncoder

class CustomJSONEncoder(DjangoJSONEncoder):

def default(self, obj):
if isinstance(obj, datetime):
return obj.strftime("%y/%m/%d %H:%M") ★ここ★
return super().default(obj)

from django.core import serializers

serializers.serialize('json', cls=CustomJSONEncoder)

これで日時型については意図した書式に変更できます。が、数値型(たとえばint)は同じ要領で以下のコードを入れてもできません。

また、そもそも属性により"円"とか"部"を付加したいのですがデータの属性名はわからないので制御できません。

        if isinstance(obj, int):

return "{:,}".format(obj)


案2(最終型)

Djangoのシリアライザは使わずにPythonのJsonシリアライザ-(json.dumps(obj))を使います。

ただし、Djangoモデルは直接変換できません。(実行エラーになります)

import json

json.dumps(fooModel)
>> TypeError: Object of type 'FooModel' is not JSON serializable

そこで、model_to_dictを使って、モデルをdict化したものをシリアライズします。

import json

from django.forms.models import model_to_dict
json.dumps(model_to_dict(FooModel))

ただし、モデルにdatetime 型が含まれているとエラーになります。

TypeError: Object of type 'datetime' is not JSON serializable

これを回避するためにdatetime型が設定されている要素に書式変換後の文字列を設定してしまいます。

from django.forms.models import model_to_dict

class FooModel(models.Model)

updated_at = models.DateTimeField('更新日時', auto_now=True, null=True)

def to_dict(self):
ret = model_to_dict(self) ★この時点では ret['update_at']datetime型★
ret['updated_at'] = self.updated_at.strftime("%y/%m/%d %H:%M") ★文字列にしてしまう★
return ret

import json

json.dumps(fooModel.to_dict())

同じ要領で数値型フィールドも任意の文字列に変換できます。

from django.forms.models import model_to_dict

class FooModel(models.Model)

total_charge = models.PositiveIntegerField('合計金額', default=0)
updated_at = models.DateTimeField('更新日時', auto_now=True, null=True)

def to_dict(self):
ret = model_to_dict(self)
ret['total_charge'] = "{:,}円".format(self.total_charge) ★ここ★
ret['updated_at'] = self.updated_at.strftime("%y/%m/%d %H:%M")
return ret

この方法のメリットとして、レスポンスに不要なフィールド(model名,主キー値)が含まれなくなることもあげられます。


おまけ

なお、モデルインスタンスが複数の場合は、to_dict()を繰り返し実行するコードを書くと冗長になります。

以下の記事にあるようにカスタマイズしたJsonシリアライザーをjson.dumpsに渡すことで冗長な記述なしで実現できます。

DjangoでJSONを使うための一考察 SQLの最適化など - グロブ

from django.db import models as django_models

from django.db.models.query import QuerySet
class CustomJSONEncoder(json.JSONEncoder):

def default(self, obj):
if isinstance(obj, django_models.Model) and hasattr(obj, 'to_dict'):
return obj.to_dict()
if isinstance(obj, QuerySet):
return list(obj)
json.JSONEncoder.default(self, obj)

from <...> import CustomJSONEncoder

json.dumps(model_list, cls=CustomJSONEncoder)

以上です。

ここまでやるとちょっと冗長な感じがでてきます。

Djangoのシリアライザ-のオプションでモデルのメソッドやプロパティをシリアル化対象に追加できるといいですね。(Django Rest frameworkだとできるようなので、余力ができたら、実装を見てみようかと。。。)