APIの日時データを壊さない設計|RFC 3339・UTC・タイムゾーンの実務ルール

「APIの日時データを壊さない設計|RFC 3339・UTC・タイムゾーンの実務ルール」の内容を表す技術イラスト

API、CSV、データベースの間でデータを渡すとき、日時は特に壊れやすい項目です。2026/09/20 10:00のような値は人には読めても、年月日の順序やタイムゾーンを機械が確定できません。9時間のずれや重複登録の原因になります。

この記事では、APIやJSONで日時を扱うための実務ルールを、RFC 3339を中心に整理します。形式を覚えるだけでなく、入力、検証、正規化、保存、表示というデータの流れまで設計します。

目次

日時データで解決すべき問題

日時には、似ているようで異なる概念があります。

  • 日付:2026-09-20のような暦上の日
  • 時刻:10:30:00のような一日の中の時点
  • タイムスタンプ:世界の時間軸上の一つの瞬間
  • UTCオフセット:UTCとの差。日本標準時なら通常+09:00
  • タイムゾーンID:Asia/Tokyoのように地域の時刻規則を示す識別子

たとえば、次の2つは表記が異なりますが、同じ瞬間です。

2026-09-20T10:00:00+09:00
2026-09-20T01:00:00Z

ZはUTCオフセット00:00を示します。

一方、2026-09-20T10:00:00にはオフセットがありません。これだけでは日本時間なのかUTCなのか判断できず、世界の時間軸上の一つの瞬間に確定できません。

RFC 3339をAPIの共通形式にする

RFC 3339は、インターネット上でタイムスタンプを交換するための日時形式を定めています。ISO 8601の全機能をそのまま使うのではなく、相互運用しやすい範囲へ絞った形式です。

基本形は次のとおりです。

YYYY-MM-DDTHH:MM:SS[小数秒]Z
YYYY-MM-DDTHH:MM:SS[小数秒]+HH:MM

例を見てみましょう。

2026-09-20T01:30:00Z
2026-09-20T10:30:00+09:00
2026-09-20T01:30:00.125Z

実務では、生成側の表記をさらに狭く統一すると連携が安定します。

  • 年は4桁、月・日・時・分・秒は2桁にする
  • 日付と時刻の間は大文字のTにする
  • UTCは大文字のZにする
  • UTC以外は+09:00のような数値オフセットを付ける
  • 小数秒を使うなら桁数をシステム間で決める
  • 日時を受け取るフィールドではオフセットなしを拒否する

RFC 3339では小文字のtやzも構文上許容されますが、生成形式を大文字へ統一するとテストやログ調査が簡単になります。

オフセットとタイムゾーンIDは別物

+09:00は、その瞬間にUTCより9時間進んでいることを示すだけです。地域の規則までは表しません。

一方、Asia/TokyoやAmerica/New_YorkのようなタイムゾーンIDは、地域ごとの過去・現在のオフセットや夏時間規則を扱うために使います。

用途によって保持すべき情報が変わります。

発生済みイベントを記録する場合

センサー受信、API実行、レコード更新など、すでに発生した瞬間を記録するなら、UTCへ正規化したタイムスタンプが中心です。

{
  "event_id": "evt_01K5R8Y2",
  "occurred_at": "2026-09-20T01:30:00Z"
}

将来の現地予定を管理する場合

「東京で2026年9月20日10時に開始」のような予定は、日時とタイムゾーンIDを分けて持つと意図を保ちやすくなります。

{
  "schedule_id": "sch_000123",
  "local_start": "2026-09-20T10:00:00",
  "time_zone": "Asia/Tokyo"
}

夏時間のある地域では、将来予定を固定オフセットだけで表すと現地時刻がずれる可能性があります。発生済みイベントの比較には、UTCへ確定した値が扱いやすくなります。

入力・変換・出力の流れ

日時連携は、次の5段階に分けると設計しやすくなります。

入力
  ↓
構文検証
  ↓
意味検証
  ↓
UTCへ正規化して保存
  ↓
利用者のタイムゾーンへ変換して表示

構文検証では、文字列が契約した形式に合うかを確認します。意味検証では、業務上許される日時かを確認します。

たとえば形式が正しくても、「計測時刻が現在より24時間以上未来なら拒否する」という業務ルールには違反し得ます。構文と意味は別々に検証します。

JSON Schemaでインターフェース契約を作る

JSONでは日時専用の型はなく、日時も文字列として表現します。そのため、フィールド名だけでなく形式をスキーマへ明記します。

{
  "$schema": "https://json-schema.org/draft/2020-12/schema",
  "type": "object",
  "required": ["event_id", "occurred_at"],
  "properties": {
    "event_id": {
      "type": "string",
      "minLength": 1
    },
    "occurred_at": {
      "type": "string",
      "format": "date-time"
    }
  },
  "additionalProperties": false
}

JSON Schema Draft 2020-12のdate-timeはRFC 3339を参照します。ただし、formatをエラー判定へ使うかは実装や設定で異なります。採用するバリデータで不正値が拒否されることをテストします。

最低限、次のケースを自動テストに含めます。

正常: 2026-09-20T01:30:00Z
正常: 2026-09-20T10:30:00+09:00
異常: 2026/09/20 10:30:00
異常: 2026-09-20T10:30:00
異常: 2026-02-30T10:30:00Z
異常: null(必須の場合)

nullとフィールド欠落を同じ意味にするかも、API契約で決めます。

Pythonで受信日時をUTCへ正規化する

Pythonでは、タイムゾーン情報を持つaware datetimeと、持たないnaive datetimeを区別します。APIのタイムスタンプを受け取る処理では、オフセットなしのnaive datetimeを通さない方が安全です。

from datetime import datetime, timezone


def normalize_rfc3339_to_utc(value: str) -> str:
    """オフセット付き日時を受け取り、UTCの秒精度へ正規化する。"""
    try:
        parsed = datetime.fromisoformat(value)
    except ValueError as exc:
        raise ValueError("日時の形式が不正です") from exc

    if parsed.tzinfo is None or parsed.utcoffset() is None:
        raise ValueError("UTCオフセットが必要です")

    normalized = parsed.astimezone(timezone.utc)
    return normalized.isoformat(timespec="seconds").replace("+00:00", "Z")


print(normalize_rfc3339_to_utc("2026-09-20T10:30:00+09:00"))
# 2026-09-20T01:30:00Z

datetime.fromisoformat()が受理する範囲とAPI契約は必ずしも同じではありません。これはRFC 3339専用バリデータではないため、JSON Schemaや専用ライブラリと組み合わせて境界で契約を確認します。

CSVと表計算ソフトへ渡すときの注意

CSVには型情報がありません。次の値も、CSV上では単なる文字列です。

event_id,occurred_at
evt_001,2026-09-20T01:30:00Z

表計算ソフトは日時らしい文字列を自動変換し、オフセットや小数秒を失う場合があります。

対策は次のとおりです。

  • 交換用CSVではRFC 3339文字列を正本とする
  • 列定義を別ファイルやデータ辞書で管理する
  • 取り込み直後に日時型へ変換し、変換失敗行を隔離する
  • Excel表示用データとシステム連携用データを同一視しない

CSVを帳票として使うのか、機械間の交換形式として使うのかを先に決めます。

冪等性と重複判定に日時だけを使わない

同じイベントが再送されるシステムでは、日時だけを重複判定キーにすると危険です。

  • 複数イベントが同じ秒に発生する
  • 送信側と受信側で小数秒の精度が異なる
  • UTC化によって文字列表現が変わる
  • 再送時に受信時刻だけが更新される

イベントには安定したevent_idを持たせ、日時は発生順や期間検索のために使います。

{
  "event_id": "evt_01K5R8Y2",
  "occurred_at": "2026-09-20T01:30:00Z",
  "received_at": "2026-09-20T01:30:02Z"
}

occurred_atは発生元、received_atは受信側が記録します。分離すれば通信遅延や再送を分析できます。

よくある失敗と設計ルール

ローカル日時をそのまま保存する

2026-09-20 10:00:00だけでは地域を特定できません。瞬間にはUTCオフセットを必須にします。

サーバーのローカルタイムへ依存する

内部処理と保存はUTCを基本にし、利用者向けの表示時に変換します。

JSTのような略称だけを使う

略称は衝突する場合があります。交換用には数値オフセット、地域規則が必要な予定にはIANAタイムゾーンIDを使います。

文字列だから文字列のまま比較する

同じ瞬間でもZと+09:00では文字列が異なります。比較前にUTCへ正規化します。文字列順を時系列順として使えるのは、表記と精度まで統一した場合に限ります。

正規表現だけで妥当性を判定する

正規表現だけでは2月30日のような値を通すおそれがあります。日時ライブラリでパースし、業務ルールも別途検証します。

セキュリティと公開情報の分離

日時から設備の稼働時間、担当者の行動、保守周期を推測できる場合があります。

公開APIやログ出力では、次を確認します。

  • 利用目的に不要なタイムゾーンや詳細時刻を公開しない
  • 監査用時刻を利用者が自由に上書きできないようにする
  • クライアント申告時刻とサーバー受信時刻を分ける

どのシステムが各時刻を生成できるかも、インターフェース契約の一部です。

再利用可能な日時データ契約

日時処理を案件ごとに書き直すのではなく、共通のデータ契約として切り出すと再利用しやすくなります。

最低限、次の項目を決めます。

field: occurred_at
meaning: イベントが発生元で発生した瞬間
type: string
format: RFC 3339 date-time
timezone_requirement: UTC offset required
canonical_output: UTC with Z
precision: seconds
nullable: false
owner: source_system
validation:
  - syntax_check
  - parse_check
  - business_range_check

この定義から、JSON Schema、Pythonの検証関数、API仕様、DB列、CSVデータ辞書、テストケースを派生できます。設計データやCAD処理ログにも同じ契約を利用できます。

まとめ

日時データ連携で重要なのは、見た目をそろえることだけではありません。どの瞬間を、誰が、どの精度とタイムゾーンで記録したのかを契約として定義することです。

実務では、次のルールから始めると安定します。

  • APIのタイムスタンプはRFC 3339形式にする
  • 瞬間を表す値にはUTCオフセットを必須にする
  • 保存・比較はUTCへ正規化する
  • 現地予定には必要に応じてIANAタイムゾーンIDを保持する
  • JSON Schemaと日時ライブラリの両方で検証する
  • null、欠落、小数秒精度、生成権限を契約で決める
  • 重複防止には日時だけでなく安定したIDを使う

日時を単なる文字列ではなく、意味と制約を持つデータとして設計すれば、API、DB、CSV、表計算、Python、自動化処理の間で同じ情報を安全に再利用できます。

参考情報

参考になったらシェアしてください
  • URLをコピーしました!
  • URLをコピーしました!

この記事を書いた人

機械設計・油圧・CAD・Python・AIなど、ものづくりに関わる技術を扱っています。工学知識を整理・構造化し、設計や自動化に再利用できる形へ変えていくことを目指しています。

目次