環境
Xcode 14.1
概要
以前も似たような記事を書いていますが、JSONのレスポンスを解析するとき、Codable
が使われますが、このときCodable対応したstructにURL型が含まれていて、それに対応するJSONのパラメータがURL(string:)
で結果がnilになる文字列(空文字、日本語、スペースetc)の場合、DecodingErrorとなってしまいます。
エラーにならない対応とするため、URLとして変換できないものはOptionalにしたいと思います。
以前のコードでは別クラスを作っての対応を行なっていましたが、別クラスを使わずに対応できる方法を紹介します。
エラーになるCodable
let json = """
{
"id": 1234,
"name": "Hello World!",
"url": ""
}
""".data(using: .utf8)
struct MyData: Codable {
let id: Int
let name: String
let url: URL?
}
let ret = try! JSONDecoder().decode(MyData.self, from: json!) // DecodingError
上記のコードは、URL?型としてurlを変換したいのですが、JSONのデータのurlはURLとして生成できない空文字のため、たとえOptionalにしてもDecodingErrorが発生してしまいます。
対応
init(from decoder: Decoder)
を実装して、エラーに対応します。
struct MyData: Codable {
let id: Int
let name: String
let url: URL?
init(from decoder: Decoder) throws {
let container: KeyedDecodingContainer<MyData.CodingKeys> = try decoder.container(keyedBy: MyData.CodingKeys.self)
self.id = try container.decode(Int.self, forKey: MyData.CodingKeys.id)
self.name = try container.decode(String.self, forKey: MyData.CodingKeys.name)
self.url = URL(string: try container.decode(String.self, forKey: MyData.CodingKeys.url)) // ←一度Stringに変換した後、URLを生成
}
}
注目するのはURLの生成部分で、一度Stringクラスを生成した後、自分のパラメータにURLとして与える、という手順をinit(from decoder: Decoder)
で行うことでDecodingErrorを回避できます。
Codableコードの自動生成
このコードはいちいち書くのは手間だと思いますので、Add Explicit Codable Implementation
で自動生成して、URL部分だけ書き換えたりするのがいいと思います。
⌘キーを押しながらstructクリックでメニューが出ます。
以下、自動生成するとこんな感じになります。
struct MyData: Codable {
let id: Int
let name: String
let url: URL?
enum CodingKeys: CodingKey {
case id
case name
case url
}
init(from decoder: Decoder) throws {
let container: KeyedDecodingContainer<MyData.CodingKeys> = try decoder.container(keyedBy: MyData.CodingKeys.self)
self.id = try container.decode(Int.self, forKey: MyData.CodingKeys.id)
self.name = try container.decode(String.self, forKey: MyData.CodingKeys.name)
self.url = try container.decodeIfPresent(URL.self, forKey: MyData.CodingKeys.url)
}
func encode(to encoder: Encoder) throws {
var container: KeyedEncodingContainer<MyData.CodingKeys> = encoder.container(keyedBy: MyData.CodingKeys.self)
try container.encode(self.id, forKey: MyData.CodingKeys.id)
try container.encode(self.name, forKey: MyData.CodingKeys.name)
try container.encodeIfPresent(self.url, forKey: MyData.CodingKeys.url)
}
}