Edited at

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

More than 1 year has passed since last update.

コメントで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)
}

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