文字っぽいの。

文字を書いています。写真も混ざります。

SwiftのJSONDecoderでISO 8601拡張形式(ミリ秒など)のDateもDecodeできるようにする

はじめに

SwiftでJSONをDecodeしてDecodable protocolを継承した構造体・クラスを生成することはよくある。その場合に、JSONに含まれるDateはISO 8601形式である場合が多い。

ISO 8601基本形式であればJSONDecoderに用意されている設定を利用すれば良い。

let jsonDecoder = JSONDecoder()
jsonDecoder.dateDecodingStrategy = .iso8601

あとはこのJSONDecoderを利用してデコードすれば良い。

課題

上述した書き方ではミリ秒などの拡張形式に対応できない。例えばこのようにミリ秒まで含められる拡張形式。

{
    "createdAt": "2024-10-10T08:00:00.110Z",
    "updatedAt": "2024-10-10T20:00:00.220Z"
}

これはdateDecodingStrategyに .ios8601 を指定したJSONDecoderではデコードに失敗する。

解決策

ありがたいことに、SwiftにはISO8601DateFormatterという、その名の通りなFormatterが存在する。

developer.apple.com

ただしこれはJSON Decoderではないので、 JSONDecoderがこれを利用できるように少しコードを書く必要がある。

struct CodableItem: Codable {
    var createdAt: Date
    var updatedAt: Date
}

let jsonString = #"""
{
    "createdAt": "2024-10-10T08:00:00.110Z",
    "updatedAt": "2024-10-10T20:00:00.220Z"
}
"""#

let jsonDecoder = JSONDecoder()
jsonDecoder.dateDecodingStrategy = .custom { decoder -> Date in
    let container = try decoder.singleValueContainer()
    let dateString = try container.decode(String.self) // 一旦Stringとして取得する

    let dateFormatter = ISO8601DateFormatter()
    dateFormatter.formatOptions = [
        .withInternetDateTime,
        .withFractionalSeconds,
        .withDashSeparatorInDate,
        .withColonSeparatorInTime,
        .withColonSeparatorInTimeZone
    ]

    // StringからDateに変換
    guard let date = dateFormatter.date(from: dateString) else {
        throw DecodingError.dataCorruptedError(in: container, debugDescription: "不正なDate")
    }

    return date
}

let jsonData = jsonString.data(using: .utf8)!
do {
    let decodedData = try jsonDecoder.decode(CodableItem.self, from: jsonData)
    print(decodedData.createdAt)
    print(decodedData.updatedAt)
} catch {
    print(error)
}

このコードで、ミリ秒が含まれたDateでもデコードすることができる。

APIによっては、EndpointやEntityの種類によってミリ秒が含まれたり含まれなかったりすることもある。そんな場合はJSONDecoderを複数作るよりも .custom {} 内部で formatOptions を変えた ISO8601DateFormatter を作って、分岐処理をすることで簡単に対応できる。