post Image
Swift4のCodableでISO8601の日付をデコードする

コメントでdecoderにdateDecodingStrategyを設定すると良いと教えてもらいました。が、フォーマットがISO8601の拡張だからなのか.iso8601ではデコードできなかったので以下のように行いました。

struct A: Codable {
    let createdAt: Date?
    let deletedAt: Date?

    private enum CodingKeys: String, CodingKey {
        case createdAt = "created_at"
        case deletedAt = "deleted_at"
    }
}

let data = """
{
"created_at": "2017-03-20T16:31:05.000Z",
"deleted_at": null
}
""".data(using: .utf8)!

let formatter = DateFormatter()
formatter.dateFormat = "yyyy-MM-dd'T'HH:mm:ss.SSS'Z'"
let decoder: JSONDecoder = JSONDecoder()
decoder.dateDecodingStrategy = .formatted(formatter)
do {
    let a = try decoder.decode(A.self, from: data)
    print(a)
} catch {
    print(error)
}

旧版

Swift4のCodableですが、Date型もCodableに準拠しています。しかしその実装を見るとDoubleからのデコードしか対応していません。ISO8601の文字列では実際にデコードしようとしてもエラーになってしまいます。

この場合おそらくinit(from:)を記述して独自にデコードする必要があると思われます。しかしinitの中でStringからDateへの変換処理を記述するのは避けたいところです。出来れば変換処理は色々な個所で利用したい。ので変換するための仕組みを作ってみました。

protocol CodingTransformerProtocol {
    associatedtype From
    associatedtype To

    func transform(_ from: From) throws -> To
}

struct DateTransformer: CodingTransformerProtocol {
    static let formatter: DateFormatter = {
        let result = DateFormatter()
        result.dateFormat = "yyyy-MM-dd'T'HH:mm:ss.SSS'Z'"
        return result
    }()

    func transform(_ from: String) throws -> Date? {
        return DateTransformer.formatter.date(from: from)
    }
}

extension KeyedDecodingContainer {
    func decode<Transformer: CodingTransformerProtocol>(with transformer: Transformer, forKey key: Key) throws -> Transformer.To where Transformer.From == String {
        return try transformer.transform(try decode(String.self, forKey: key))
    }

    func decodeIfPresent<Transformer: CodingTransformerProtocol>(with transformer: Transformer, forKey key: Key) throws -> Transformer.To where Transformer.From == String, Transformer.To: ExpressibleByNilLiteral {
        guard let decoded = try decodeIfPresent(String.self, forKey: key) else { return nil }
        return try transformer.transform(decoded)
    }
}

CodingTransformerProtocolに準拠したDateTransformerを作成して、以下のように利用します。

struct A: Codable {
    let createdAt: Date?
    let deletedAt: Date?

    private enum CodingKeys: String, CodingKey {
        case createdAt = "created_at"
        case deletedAt = "deleted_at"
    }

    init(from decoder: Decoder) throws {
        let container = try decoder.container(keyedBy: CodingKeys.self)
        createdAt = try container.decode(with: DateTransformer(), forKey: .createdAt)
        deletedAt = try container.decodeIfPresent(with: DateTransformer(), forKey: .deletedAt)
    }
}

let data = """
{
"created_at": "2017-03-20T16:31:05.000Z",
"deleted_at": null
}
""".data(using: .utf8)!

let decoder: JSONDecoder = JSONDecoder()
do {
    let a = try decoder.decode(A.self, from: data)
    print(a)
} catch {
    print(error)
}

より簡単な方法があれば教えてほしいです…


『 Swift 』Article List