🕸️

[Swift]壊れた値があってもJSONデコードを失敗させないテクニック

2023/08/23に公開

Codableの構造体にURLを宣言すると、jsonの値がURLである場合に限りURL型にデコードしてくれます。

let json = """
{
    "url" : "https://google.com"
}
"""
struct JSON: Codable {
    let url: URL
}
let data = Data(json.utf8)
let decoder = JSONDecoder()
let decoded = try decoder.decode(JSON.self, from: data)
XCTAssertEqual(decoded.url, URL(string: "https://google.com"))

しかし、値がURLで無い場合デコードが失敗してしまいます。

この問題に対して2つの解決策を紹介します。

String型で保持する

struct JSON: Codable {
  let url: String
  var URL: URL? { URL(string: url) }
}

String型で保持することでどんな値が来てもJSONDecoderは失敗しなくなります。
この方法のメリットは、Stringが保持されていることでURLに出来なかった場合に元の文字列を確認できます。
URLはComputed Propertyで逐次計算される点は注意しましょう。
また、プロパティ名が被りがちな点も注意が必要です。
String型のプロパティをurlStringにしたい場合は、CodingKeysなどを活用する必要があり、冗長な書き方になります。

rawValueとURLを持った構造体やenumを用意しておくと多少綺麗になるかもしれませんが、独自の型をURLとして渡せないので使い勝手は悪くなります。

JSONDecoderを拡張する

KeyedDecodingContainerのextensionでデコードの挙動をオーバーライドすることができます。

extension KeyedDecodingContainer {
    func decodeIfPresent(_ type: URL.Type, forKey key: Key) throws -> URL? {
        try? decode(type, forKey: key)
    }
}

この拡張をすることでURLのデコードに失敗する場合はnilが入るようになります。

let json = """
{
    "url" : "https://google.com/あ"
}
"""
struct JSON: Codable {
    let url: URL?
}
let data = Data(json.utf8)
let decoder = JSONDecoder()
let decoded = try decoder.decode(JSON.self, from: data)
XCTAssertNil(decoded.url)

この方法では、URL型へのデコードの失敗原因や元のStringが残らない点に注意が必要です。
また、この拡張が行われているかをパッと見て把握しにくい点にも注意しましょう。

Discussion