日付をJSON文字列にするには
日付や日時を含むデータを JSON 文字列にする基本形を各言語で示す。
変換できずに例外になる言語と自動で文字列になる言語の違い、読み戻しても日付型に戻らない点、オフセットが残るかどうかまでを扱う。
公開:
選んだ言語は、ほかのサンプルコードを開いたときも最初に表示する。
Python
import json
from datetime import datetime, timedelta, timezone
JST = timezone(timedelta(hours=9))
event = {"name": "定例会", "starts_at": datetime(2026, 10, 22, 9, 5, tzinfo=JST)}
# 既定のままでは datetime を文字列にできない
try:
json.dumps(event)
except TypeError as e:
print("TypeError:", e) # TypeError: Object of type datetime is not JSON serializable
# default には「変換できない値」が来たときだけ呼ばれる関数を渡す
def to_jsonable(o):
if isinstance(o, datetime):
return o.isoformat()
raise TypeError(f"{type(o).__name__} は JSON にできない")
text = json.dumps(event, ensure_ascii=False, default=to_jsonable)
print(text) # {"name": "定例会", "starts_at": "2026-10-22T09:05:00+09:00"}
# 読み戻しても文字列のまま。日付型に戻すには自分で変換する
back = json.loads(text)
print(type(back["starts_at"]).__name__) # str
print(datetime.fromisoformat(back["starts_at"]).utcoffset()) # 9:00:00標準の json.dumps は datetime を扱えず TypeError になるので、default に変換関数を渡して isoformat() の文字列にする。default は変換できない値が来たときだけ呼ばれるため、想定外の型には TypeError を投げ直して黙って壊れたデータを出さないようにする。
JavaScript
const event = { name: "定例会", startsAt: new Date("2026-10-22T09:05:00+09:00") };
// Date は toJSON を持つので、JSON.stringify が自動で ISO 8601 の文字列にする
const text = JSON.stringify(event);
console.log(text); // {"name":"定例会","startsAt":"2026-10-22T00:05:00.000Z"}(UTC に直されて +09:00 は残らない)
// 読み戻しても文字列のまま。Date に戻すには reviver で変換する
const back = JSON.parse(text);
console.log(typeof back.startsAt, back.startsAt instanceof Date); // string false
const revived = JSON.parse(text, (key, value) => (key === "startsAt" ? new Date(value) : value));
console.log(revived.startsAt instanceof Date, revived.startsAt.getTime() === event.startsAt.getTime()); // true trueDate は toJSON メソッドを持つので、JSON.stringify に渡すだけで ISO 8601 の文字列になる。
読み戻した値は Date ではなく文字列なので、JSON.parse の第 2 引数の reviver で new Date に変換する。
TypeScript
type Meeting = { name: string; startsAt: Date };
// JSON に入った形は Date ではなく string なので、別の型で受ける
type MeetingJson = { name: string; startsAt: string };
const meeting: Meeting = { name: "定例会", startsAt: new Date("2026-10-22T09:05:00+09:00") };
const text = JSON.stringify(meeting);
console.log(text); // {"name":"定例会","startsAt":"2026-10-22T00:05:00.000Z"}
// JSON.parse の戻り値は any なので、型を付けただけでは Date に変わらない
const wire = JSON.parse(text) as MeetingJson;
const back: Meeting = { name: wire.name, startsAt: new Date(wire.startsAt) };
console.log(back.startsAt instanceof Date, back.startsAt.getTime() === meeting.startsAt.getTime()); // true true実行時の挙動は JavaScript と同じで、型だけが加わる。JSON.parse の戻り値を as で Date を持つ型に断言しても実体は文字列のままなので、JSON に入る形の型と、アプリ内で使う型を分けて変換する。
Go
package main
import (
"encoding/json"
"fmt"
"time"
)
type Event struct {
Name string `json:"name"`
StartsAt time.Time `json:"startsAt"`
EndsAt *time.Time `json:"endsAt,omitempty"` // 未定は nil にすると出力されない
}
func main() {
jst := time.FixedZone("JST", 9*60*60)
e := Event{Name: "定例会", StartsAt: time.Date(2026, 10, 22, 9, 5, 0, 0, jst)}
// time.Time は RFC 3339 の文字列になり、オフセットも残る
b, _ := json.Marshal(e)
fmt.Println(string(b)) // {"name":"定例会","startsAt":"2026-10-22T09:05:00+09:00"}
// 読み戻すと time.Time に戻る
var back Event
if err := json.Unmarshal(b, &back); err != nil {
panic(err)
}
fmt.Println(back.StartsAt.Equal(e.StartsAt)) // true
// ゼロ値の time.Time は省略されず、0001 年として出力される
z, _ := json.Marshal(Event{Name: "未定"})
fmt.Println(string(z)) // {"name":"未定","startsAt":"0001-01-01T00:00:00Z"}
}time.Time は MarshalJSON を実装していて、RFC 3339 の文字列になる。Unmarshal で time.Time にも戻るため、変換コードは要らない。
未設定のときに出力したくないフィールドは、構造体のままだと omitempty が効かない。
Go 1.24 以降は omitzero を付ければ time.Time のままゼロ値を省略でき、それより前の版では *time.Time にして nil を入れる。
Rust
// Cargo.toml に次を追加する
// chrono = { version = "0.4", features = ["serde"] }
// serde = { version = "1", features = ["derive"] }
// serde_json = "1"
use chrono::{DateTime, FixedOffset};
use serde::{Deserialize, Serialize};
#[derive(Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
struct Event {
name: String,
starts_at: DateTime<FixedOffset>,
}
fn main() -> Result<(), serde_json::Error> {
let event = Event {
name: "定例会".to_string(),
starts_at: DateTime::parse_from_rfc3339("2026-10-22T09:05:00+09:00").unwrap(),
};
// serde feature を有効にした chrono は RFC 3339 の文字列にする。オフセットも残る
let text = serde_json::to_string(&event)?;
println!("{text}"); // {"name":"定例会","startsAt":"2026-10-22T09:05:00+09:00"}
// 読み戻すと DateTime<FixedOffset> に戻る
let back: Event = serde_json::from_str(&text)?;
println!("{}", back.starts_at == event.starts_at); // true
Ok(())
}chrono は serde feature を有効にすると DateTime を RFC 3339 の文字列として Serialize / Deserialize できる。
feature を付け忘れると、DateTime<FixedOffset> に Serialize が実装されていないという E0277 のコンパイルエラーになる。
Java
// jackson-databind と jackson-datatype-jsr310 が必要
import com.fasterxml.jackson.databind.ObjectMapper;
import com.fasterxml.jackson.databind.SerializationFeature;
import com.fasterxml.jackson.datatype.jsr310.JavaTimeModule;
import java.time.OffsetDateTime;
public class Main {
public static void main(String[] args) throws Exception {
var event = new Event("定例会", OffsetDateTime.parse("2026-10-22T09:05:00+09:00"));
// 標準の ObjectMapper は java.time を扱えず、例外になる
try {
new ObjectMapper().writeValueAsString(event);
} catch (Exception e) {
System.out.println(e.getClass().getSimpleName());
}
// JavaTimeModule を登録し、数値ではなく ISO 8601 の文字列で書く設定にする
ObjectMapper mapper = new ObjectMapper()
.registerModule(new JavaTimeModule())
.disable(SerializationFeature.WRITE_DATES_AS_TIMESTAMPS);
String text = mapper.writeValueAsString(event);
System.out.println(text); // {"name":"定例会","startsAt":"2026-10-22T09:05:00+09:00"}
Event back = mapper.readValue(text, Event.class);
System.out.println(back.startsAt().isEqual(event.startsAt())); // true
}
}
record Event(String name, OffsetDateTime startsAt) {}Jackson の標準の ObjectMapper は java.time の型を扱えず InvalidDefinitionException になるため、jackson-datatype-jsr310 を足して JavaTimeModule を登録する。
さらに WRITE_DATES_AS_TIMESTAMPS を無効にしないと、日時が文字列ではなく秒数の数値で出力される。
C#
using System.Text.Encodings.Web;
using System.Text.Json;
var e = new Event("定例会", new DateTimeOffset(2026, 10, 22, 9, 5, 0, TimeSpan.FromHours(9)));
var options = new JsonSerializerOptions
{
PropertyNamingPolicy = JsonNamingPolicy.CamelCase,
Encoder = JavaScriptEncoder.UnsafeRelaxedJsonEscaping, // 日本語をそのまま出す
};
// DateTimeOffset は ISO 8601 の文字列になり、オフセットも残る
string text = JsonSerializer.Serialize(e, options);
Console.WriteLine(text); // {"name":"定例会","startsAt":"2026-10-22T09:05:00+09:00"}
// 読み戻すと DateTimeOffset に戻る
var back = JsonSerializer.Deserialize<Event>(text, options)!;
Console.WriteLine(back.StartsAt == e.StartsAt); // True
// DateTime は Kind によって出力が変わる。Unspecified はオフセットが付かない
Console.WriteLine(JsonSerializer.Serialize(new DateTime(2026, 10, 22, 9, 5, 0, DateTimeKind.Unspecified)));
Console.WriteLine(JsonSerializer.Serialize(new DateTime(2026, 10, 22, 9, 5, 0, DateTimeKind.Utc)));
record Event(string Name, DateTimeOffset StartsAt);System.Text.Json は DateTimeOffset と DateTime を追加設定なしで ISO 8601 の文字列にする。
オフセットを残したいなら DateTimeOffset を選ぶ。DateTime は Kind によって出力が変わり、Unspecified ではオフセットも Z も付かない。
つまずき
日付を含むデータを JSON にするとき、何も指定しないと言語によって結果が三通りに分かれる。
Python は json.dumps が TypeError: Object of type datetime is not JSON serializable で止まる。
Java の Jackson は、標準の ObjectMapper が java.time.OffsetDateTime を受け取ると Java 8 date/time type java.time.OffsetDateTime not supported by default という InvalidDefinitionException を投げる。
Rust も chrono の serde feature を有効にしないとコンパイルが通らない(E0277)。
一方、JavaScript・Go・C# は何もしなくても文字列になる。
止まる言語は原因が分かりやすいが、変換されてしまう言語は、出てきた文字列が自分の想定した形かを確かめないと見逃す。
読み戻しても日付型には戻らない
JSON に日付型は無く、文字列として書かれるだけである。
そのため JavaScript と Python の json.loads では、読み戻した値は Date や datetime ではなく文字列のままになる(上のサンプルで back.startsAt instanceof Date は false、Python の型は str と確かめた)。
使う側で new Date(value) や datetime.fromisoformat(value) に変換する処理が必要で、キー名で判定する reviver を用意しておくと変換の漏れを一か所にまとめられる。
Go・Rust・Java・C# は読み込み先の型が決まっているので、同じ形式の文字列であればそのまま日付型に戻る。
オフセットが残るとは限らない
文字列にしたときにタイムゾーンのオフセットが残るかは、言語と型で変わる。
JavaScript の Date は toJSON が UTC に直すので、+09:00 で作った日時でも 2026-10-22T00:05:00.000Z と出力され、元のオフセットは戻らない(同じ瞬間を指してはいる)。
Python の datetime.isoformat() はタイムゾーンを持たない naive な値だとオフセットを付けず、2026-10-22T09:05:00 になる。
C# の DateTime も Kind が Unspecified だと同様で、Utc だと末尾に Z が付く。
利用者のオフセットを残したい場合は、Python なら tzinfo を付けた aware な値、C# なら DateTimeOffset を使う。
文字列の形を読む側に合わせる
形式を ISO 8601 にそろえておけば、ほとんどの言語がそのまま読める。
ただし Python の datetime.fromisoformat は、3.9 では末尾が Z の 2026-10-22T00:05:00Z を ValueError: Invalid isoformat string で拒否し、3.12 では読める(どちらも実行して確かめた)。
JavaScript の出力は常に Z で終わるので、古い Python が読む JSON に JavaScript の Date をそのまま入れると読み込みで落ちる。
Java の Jackson は WRITE_DATES_AS_TIMESTAMPS を無効にしないと 1792627500.000000000 のような秒数になり、他の言語が日時として読めなくなる。
送る側と読む側で、日付を文字列にするのか数値にするのか、オフセットを含めるのかを先に決めておく。