夜中にjsonのデコードで泣かないために

夜中にコーディングしていて、サーバーAPIから取得したjsonデコードに失敗したんだけど、もう疲れていて詳しく調べるのがめんどくさいことってありませんか?

そんな時にはDecodingErrorをみてみましょう。

jsonデコードのエラー

iOSの標準のjson decoderはJSONDecoderですが、このクラスは、デコードに失敗したときに、詳細な情報を返してくれます。

探しているキー情報がないときにはDecodingError.keyNotFound、データ自体が壊れている場合はDecodingError.dataCorrupted、値の型が違う場合にはDecodingError.typeMismatch、値がない場合にはDecodingError.valueNotFound、などがよくあるエラーですね。

エラー情報には、エラーが発生したキーの情報なども含まれているので、一括catchをするのではなく、デバッグログなどに詳細情報を出したりしておくとデバッグしやすいですね。

下記のコードは、myURLから取得したデータをMySuperDataにデコードしたときのエラー処理です。

Errorから取得したデバッグ情報をStringとし、myErrorPublisherというPublisherで他のクラスに通知しています。 (Publisherの使い方については、Combine 最初の一歩 - Toyship.orgをみてください。)

エラー情報にurlも含んでおくとデバッグの時に便利です。

do{

    let myURL = URL(string: "http://www.test.com")!
    let (data, _) = try await URLSession.shared.data(from: myURL)
    
    let decoder = JSONDecoder()
    
    let mySuperData = try decoder.decode(MySuperData.self, from: data)
    
}
catch DecodingError.keyNotFound(let key, _) {
    let errorString = "url[\(myURL)]に\(key.stringValue) という名前の要素が見つかりません。"
    let errorToSend = MySetupError.decodeError(errorString)
    myErrorPublisher.send(completion: .failure(errorToSend))
}
catch DecodingError.dataCorrupted(_) {
    let errorString = "url[\(myURL)]から取得したデータがこわれています。"
    let errorToSend = MySetupError.decodeError(errorString)
    myErrorPublisher.send(completion: .failure(errorToSend))
}
catch DecodingError.typeMismatch(let type, let context) {
    var pathString: String = ""
    for pathInfo in context.codingPath{
        let p = pathInfo.stringValue
        pathString += "\\" + p
    }
    let errorString = "url[\(myURL)]から取得したデータの [\(pathString)]の要素のタイプが[\(type)]であると予想していたんですが、違いました。"
    let errorToSend = MySetupError.decodeError(errorString)
    myErrorPublisher.send(completion: .failure(errorToSend))
}
catch DecodingError.valueNotFound(let type, _){
    let errorString = "url[\(myURL)]から取得したデータ[\(type)]の値がNULLでした。"
    let errorToSend = MySetupError.decodeError(errorString)
    myErrorPublisher.send(completion: .failure(errorToSend))
}
catch let error as NSError{

    if error.domain == NSURLErrorDomain{
        let errorToSend = MySetupError.networkError(error.localizedDescription)
        myErrorPublisher.send(completion: .failure(errorToSend))
    }
    else{
        let errorToSend = MySetupError.anotherError(error.localizedDescription)
        myErrorPublisher.send(completion: .failure(errorToSend))
    }
}
catch{
    let errorToSend = MySetupError.anotherError(error.localizedDescription)
    myErrorPublisher.send(completion: .failure(errorToSend))
}

エラーはこんな感じです。

enum MySetupError: Swift.Error {
    case decodeError(String)
    case networkError(String)
    case anotherError(String)
}