できない.dev

日付を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 true

Date は 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 のコンパイルエラーになる。

できないときRust「failed to resolve: use of undeclared crate or module」の原因と直し方

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 を無効にしないと、日時が文字列ではなく秒数の数値で出力される。

できないときJava「package does not exist」が javac で出る原因と直し方

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 のような秒数になり、他の言語が日時として読めなくなる。
送る側と読む側で、日付を文字列にするのか数値にするのか、オフセットを含めるのかを先に決めておく。

このコードで動いた?