LoginSignup
0
1

More than 1 year has passed since last update.

Swift JSON+CodableでURL変換できない文字列をOptionalにする

Posted at

環境

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クリックでメニューが出ます。

スクリーンショット 2022-11-08 10.50.01.png

以下、自動生成するとこんな感じになります。

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)
    }
}

0
1
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
0
1