写真に重ねて綴った言葉へ「◯月◯日 に追記」と日付を添える機能を作っていて、レスポンスに日付を足した段階で1日ずれているのに気づいた時の記録です。
追記の日付を出すために、レスポンスに書いた日時を足した。
return {
"text": meaning.text,
# 綴った日時(created_at)を文字列にしてフロントへ渡す
"writtenAt": meaning.created_at.isoformat() if meaning.created_at else None,
}これで正しく動いているように見えた。JST の朝から夕方に書いたものは、確かに正しい日付が出ます。
問題は深夜だった。
何が起きるか
日本時間 7月21日の午前1時30分に綴ったとします。9時間戻すと、UTC ではこれは 7月20日の16時30分。時刻を戻したせいで、日付が前の日にまたいでいます。
USE_TZ = True の Django は、データベースに UTC で保存し、モデルから読み出した datetime も UTC のまま返します。settings.TIME_ZONE = "Asia/Tokyo" を設定していても、それは自動では効きません。
>>> meaning.created_at.isoformat()
'2026-07-20T16:30:00+00:00' # 7月20日日付の部分だけを取って表示すると「7月20日 に追記」になります。本人が書いたのは 7月21日の未明なのに、前の日として出ます。
気づきにくいのは、片方だけ正しく見えているから
厄介だったのは、同じプロジェクトの中に正しく返している別の窓口(エンドポイント)が既にあったことだった。
ずれているのは分かったが、なぜ片方だけ正しいのかはすぐには見当がつかなかった。実装は AI と進めているので、日時を返している箇所を洗い出してもらって並べた。返し方が2通りあった。
DRF(Django REST Framework)の DateTimeField は、シリアライズするときに settings.TIME_ZONE でローカル時刻に直してくれます。だから一覧を返す API はちゃんと +09:00 で返っていた。
| 返している場所 | 実装 | 7/21 01:30 JST の綴りが |
|---|---|---|
| 一覧の API | DRF の DateTimeField |
2026-07-21T01:30:00+09:00 |
| 作品ページの解決 | 生の isoformat() |
2026-07-20T16:30:00+00:00 |
どちらのレスポンスを見ているかで日付が変わります。片方だけ見ている限り、バグに見えません。
直し方
表示のために取り出すときに、明示的にローカル時刻へ直します。
from django.utils import timezone
return {
"text": meaning.text,
# settings.TIME_ZONE(Asia/Tokyo)のローカル時刻へ。
# 素の isoformat() だと UTC のままで、JST 00:00〜09:00 に綴った分が1日前になる。
"writtenAt": timezone.localtime(meaning.created_at).isoformat() if meaning.created_at else None,
}timezone.localtime() は settings.TIME_ZONE を見て変換してくれます。DRF が内部でやっているのと同じことを、手書きのレスポンスでも通す形になります。
これで両方の経路が +09:00 で揃った。
学び
- 「日付」は datetime だけでは決まりません。datetime + タイムゾーンで初めて決まります。 UTC のままの値から日付部分を切り出せば、出てくるのは「UTC での日付」であって、書いた人にとっての日付ではありません。切り出す前に、どのタイムゾーンで読むかを必ず決めます。
- フレームワークが自動でやってくれる場所と、自分で書く場所が混ざると、同じデータが2つの顔を持ちます。 レスポンスを手で組み立てる関数を書いたら、同じデータを返している他の口と形を突き合わせます。
- 時差のバグは「ずれる時間帯」でしか出ません。 日中しか触っていないと一生気づかないので、テストを書くならわざとその時間帯のデータを入れます。
def test_entries_written_at_is_local_time_not_utc(self):
# UTC 7/20 16:30 = JST 7/21 01:30。ここがずれる時間帯。
...
written = out["photos"][str(a.id)]["entries"][0]["writtenAt"]
# 返ってきた文字列が JST の日付で始まり、+09:00 で終わる
self.assertTrue(written.startswith("2026-07-21T01:30"), written)
self.assertTrue(written.endswith("+09:00"), written) # UTC のままなら +00:00この記事は2026年7月時点の自分の理解で書いています。仕様や認識が変わってる可能性があります。