はじめに
私は、よく Django + iOS(Swift) を使ってサービスを作ったり作らなかったりしているわけですが、以前このようなことになりました。
例えば、 Django で django.utils.timezone.now
なものを DRF の rest_framework.response.Response
に直で返したりとか馬鹿なことをやってたら
iOS 側の API クライアントがデコードエラー吐き出してなんだろうって思って調査したらフォーマットが rest_framework.serializer.Serializer
とかを通さないとそのまま django.http.response.HttpResponseBase
の中身に入ってしまってバグってしまうんだなっていうことがありました。
詳しくは、こちらですね。
bytes
または str
でないとそのまま str
によって展開されてしまうっていうやつ
>>> from datetime import datetime >>> str(datetime.now()) '2019-06-18 14:33:53.148964'
という感じ。
それで本題。これを Swift の JSONDecoder
で対応させるか、 Django 側で一括で処理させるかっていうやつ。
環境
そもそも Codable
ってなによ
とりあえずみて(丸投げ
struct Response: Codable { let date: Date private enum CodingKeys: String, CodingKey { case date } }
今回はこの形でレスポンスをデコードしたい Codable (== Decodable & Encodable)
に準拠した Response
の struct
を用意しました。
結果から先に
Django の JsonResponse
を使用したパターン
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 が本来もっている JsonResponse
で API 受け口を用意している場合。
JSONDecoder
で "yyyy-MM-dd'T'HH:mm:ss.SSS'Z'"
を .formatted
で指定してあげるように DateFormatter
を作るとうまくいく。
ちなみにレスポンスは以下
{"date": "2019-06-18T05:42:47.925Z"}
DRF の Response
のみを使用したパターン
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()})
これもさっきの Django の JsonResponse
とよく似た形。はじめにでもちょっと触れたように datetime
がそのまま str
で展開されてしまうので、
同じように "yyyy-MM-dd'T'HH:mm:ss.SSS'Z'"
を設定してあげる。
こちらもレスポンスは以下
{"date":"2019-06-18T05:42:26.236135Z"}
DRF の Response
と Serializer
を使用したパターン
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
を展開すると DRF の DATETIME_FORMAT
のデフォルト値が設定される。
{"date":"2019-06-18T14:45:08.387081+09:00"}
こんな感じの ISO8601 フォーマット。
しかし、これだと以下のように JSONDecoder
の dateDecodingStrategy
を iso8601
にして実装してるとデコードエラーがでる。(マジ意味わからん...
ということでここで設定する値は "yyyy-MM-dd'T'HH:mm:ss.SSSZ"
という形になった。
DRF 使ってるんならもうちょっと楽になるでしょ?
調べた。頑張った。あった。
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
になった。気持ちいい!!
DRF の DATETIME_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
の順番。
そもそもなんかおかしい気がする。
ここらへんのソートはそもそもクライアント側ではなくサーバ側でするべきだという想いが強く出た。疲れた。
ソースコード
$ docker build -t codable_datetime . $ docker run -p 8000:8000 codable_datetime:latest
最後に
最終的に Foundation
のバグか何か知らんけど予期せぬ動作を確認してしまった。
ここらへんの詳しいことはまた今度調査とかできたらやっておきたいな。