『入る学科間違えた高専生』の日記

プログラミングのコードを書いたりする予定です。あとは日記等。あといつまで高専生やねん

Django (DRFを含む) で日付をレスポンスに含めて Swift の Codable で正常にデコードさせる

はじめに

私は、よく Django + iOS(Swift) を使ってサービスを作ったり作らなかったりしているわけですが、以前このようなことになりました。

例えば、 Djangodjango.utils.timezone.now なものを DRFrest_framework.response.Response に直で返したりとか馬鹿なことをやってたら
iOS 側の API クライアントがデコードエラー吐き出してなんだろうって思って調査したらフォーマットが rest_framework.serializer.Serializer とかを通さないとそのまま django.http.response.HttpResponseBase の中身に入ってしまってバグってしまうんだなっていうことがありました。

詳しくは、こちらですね。

github.com

bytes または str でないとそのまま str によって展開されてしまうっていうやつ

>>> from datetime import datetime
>>> str(datetime.now())
'2019-06-18 14:33:53.148964'

という感じ。

それで本題。これを Swift の JSONDecoder で対応させるか、 Django 側で一括で処理させるかっていうやつ。

環境

そもそも Codable ってなによ

qiita.com

とりあえずみて(丸投げ

struct Response: Codable {
    let date: Date

    private enum CodingKeys: String, CodingKey {
        case date
    }
}

今回はこの形でレスポンスをデコードしたい Codable (== Decodable & Encodable) に準拠した Responsestruct を用意しました。

結果から先に

DjangoJsonResponse を使用したパターン

from django.http.response import JsonResponse
from django.utils.timezone import now
from django.views.generic import View


class DefaultJsonResponseView(View):

    def get(self, request, *args, **kwargs):
        return JsonResponse({'date': now()})

という簡単な DRF とかを使わない Django が本来もっている JsonResponseAPI 受け口を用意している場合。
JSONDecoder"yyyy-MM-dd'T'HH:mm:ss.SSS'Z'".formatted で指定してあげるように DateFormatter を作るとうまくいく。

ちなみにレスポンスは以下

{"date": "2019-06-18T05:42:47.925Z"}

DRFResponse のみを使用したパターン

from django.utils.timezone import now
from rest_framework.response import Response
from rest_framework.views import APIView


class WithoutSerializerAPIView(APIView):

    def get(self, request, *args, **kwargs):
        return Response(data={'date': now()})

これもさっきの DjangoJsonResponse とよく似た形。はじめにでもちょっと触れたように datetime がそのまま str で展開されてしまうので、 同じように "yyyy-MM-dd'T'HH:mm:ss.SSS'Z'" を設定してあげる。

こちらもレスポンスは以下

{"date":"2019-06-18T05:42:26.236135Z"}

DRFResponseSerializer を使用したパターン

from django.utils.timezone import now
from rest_framework import serializers
from rest_framework.response import Response
from rest_framework.views import APIView


class WithSerializerAPIView(APIView):

    class Model:

        def __init__(self, date):
            self.date = date

    class ModelSerializer(serializers.Serializer):

        date = serializers.DateTimeField()

        class Meta:
            fields = ('date',)

    def get(self, request, *args, **kwargs):
        model = self.Model(date=now())
        data = self.ModelSerializer(model).data
        return Response(data=data)

ちょっとだけ楽して書いてしまったけど、こんな感じ。 Serializer 経由で datetime を展開すると DRFDATETIME_FORMAT のデフォルト値が設定される。

{"date":"2019-06-18T14:45:08.387081+09:00"}

こんな感じの ISO8601 フォーマット。
しかし、これだと以下のように JSONDecoderdateDecodingStrategyiso8601 にして実装してるとデコードエラーがでる。(マジ意味わからん...

ということでここで設定する値は "yyyy-MM-dd'T'HH:mm:ss.SSSZ" という形になった。

DRF 使ってるんならもうちょっと楽になるでしょ?

調べた。頑張った。あった。

www.django-rest-framework.org

settings.py に以下の様に設定すると気持ちがいいことになる

REST_FRAMEWORK = {
    'DATETIME_FORMAT': '%Y-%m-%dT%H:%M:%S%z',
}

これで返ってくるレスポンスがこんな感じ

{"date":"2019-06-18T14:56:28+0900"}

デフォルトの設定でのレスポンスは、これだった。

{"date":"2019-06-18T14:45:08.387081+09:00"}

つまりは、 JSONDecoder にはミリ秒は必要としていない!?ということがわかった。
確かに Playground とかでも

print(Date())

2019-06-18 06:00:08 +0000

というような感じにミリ秒が完全に無視されている。(これが原因かはちょっと詳しくないのでエロい人教えて)

ちょっとだけ脱線したけど、これで JSONDecoder での実装は

let decoder = JSONDecoder()
decoder.dateDecodingStrategy = .iso8601

になった。気持ちいい!!

DRFDATETIME_FORMAT からミリ秒を消すデメリット

1つだけ浮かび上がった。

例えばの話だよ??(僕の知る限り現実でこんなことは断じて起きていない)
自動インクリメントすることができないプライマリーキーが設定されているデータベーステーブルのデータを返したい。
しかし100ユーザが1秒以内に1つずつ POST してきた場合、( 2019-06-18 06:00:08.000000 〜 2019-06-18 06:00:08.999999 の間に全部が通る)
100件のデータが作成されますね。
それでまぁ、 ないとは思いますけど、 クライアント側で日付の並び順でソートしろとか言われたとしましょう。
あれ?ここの100件、どうもソートがうまくいかない??? という感じになるんじゃないかと思う。

やってみたらそうなった。

var data: [[String: Date]] = []
/* 頑張っていっぱいサーバから取得する
 ここはあえてデバッグでわかりやすいように1回辞書にした */
data.append(["{{ number }}": value.date])

data.sorted(by: { $0[$0.keys.first!]! < $1[$1.keys.first!]! })
let formatter = DateFormatter()
formatter.dateFormat = "yyyy-MM-dd HH:mm:ss.SSSSSS"

data.forEach {
    let key = $0.keys.first!
    print(key)
    print(formatter.string(from: $0[key]!))
    print("===================")
}

というような感じですると、ミリ秒のところがちょんぎられてしまっているので見事にぐちゃぐちゃ〜になる。
特に Realm のテーブルの表示もミリ秒は表示されていないのでここらへんの実装はちょっと相談とかしたほうがいいかなって感じた。

上のやつでちょっと適当に実装したやつだけど

default
2019-06-18 06:39:01.474000
===================
with_0
2019-06-18 15:39:01.000000
===================
with_1
2019-06-18 15:39:01.000000
===================
without
2019-06-18 06:39:01.476000
===================
with_2
2019-06-18 15:39:01.000000
===================

というような結果になった。リクエストの順番的には、 default -> without -> with_0 -> with_1 -> with_2 の順番。
そもそもなんかおかしい気がする。

ここらへんのソートはそもそもクライアント側ではなくサーバ側でするべきだという想いが強く出た。疲れた。

ソースコード

bitbucket.org

$ docker build -t codable_datetime .
$ docker run -p 8000:8000 codable_datetime:latest

最後に

最終的に Foundation のバグか何か知らんけど予期せぬ動作を確認してしまった。
ここらへんの詳しいことはまた今度調査とかできたらやっておきたいな。