Search code examples
swiftcodable

Swift Struct Decoder initializer error when decoding JSON


I'm trying to better understand Decoder initializers and JSON decoding. This code runs and produces the expected result:

let json = """
[
    {
        "firstName": "Jon",
        "lastName": "Dough"
   },
    {
        "firstName": "Jane",
        "lastName": "Dough"
    }
]
""".data(using: .utf8)!

struct Character: Codable {
    var firstName: String
    var lastName: String
    
     enum CodingKeys: String, CodingKey {
        case
            firstName,
            lastName
    }
 
    init(from decoder: Decoder) throws {
        
        let data = try decoder.container(keyedBy: CodingKeys.self)

        firstName = try data.decode(String.self, forKey: .firstName)
        lastName = try data.decode(String.self, forKey: .lastName)
    }
}

let decoder = JSONDecoder()
func doDecode() -> [Character] {
    do {
        var cast = try decoder.decode([Character].self, from: json)
        return cast
    } catch {
        print("Error")
    }
    return []
}

let cast = doDecode()

print("# Cs \(cast.count)")

When I attempt to add a helper function,

extension Character {
    private func tryDecode<T: Codable>(_ data: KeyedDecodingContainer<CodingKeys>, type: T.Type, forKey: CodingKeys, empty: T) -> T {
        do {
            let result = try data.decode(T.self, forKey: forKey)
            return result
        } catch { return empty }
    }
}

I get a compiler error:

Variable 'self.firstName' used before being initialized

When I call it from the initializer:

init(from decoder: Decoder) throws {
    let data = try decoder.container(keyedBy: CodingKeys.self)
    firstName = tryDecode(data, type: String.self, forKey: Character.CodingKeys.firstName, empty: "")
    lastName = tryDecode(data, type: String.self, forKey: Character.CodingKeys.lastName, empty: "")
}

I've entered "forKey:" with and without the full path. Same result.

What am I doing wrong? Why doesn't the compiler know that the "forKey:" parameter refers to the enum, not self.firstName?

I'm running the latest Xcode & macOS releases in an Xcode playground.


Solution

  • Not related to your question but Character is a native Swift type. You should choose another name to your custom structure to avoid having add Swift prefix when referring to the native type.

    Regarding the actual issue, you have declared your method as an instance method so it needs all properties to be initialized before trying to call it. Considering that your method doesnt require to access self you can declare your method as static to avoid having to initialize all properties of your struct. When calling your method you will need to add the structure prefix/.

    Char.tryDecode(...
    

    struct Char: Codable {
        var firstName: String
        var lastName: String
        
         enum CodingKeys: String, CodingKey {
            case
                firstName,
                lastName
        }
     
        init(from decoder: Decoder) throws {
            let data = try decoder.container(keyedBy: CodingKeys.self)
            firstName = Char.tryDecode(data, type: String.self, forKey: Char.CodingKeys.firstName, empty: "")
            lastName = Char.tryDecode(data, type: String.self, forKey: Char.CodingKeys.lastName, empty: "")
        }
    }
    

    extension Char {
        private static func tryDecode<T: Codable>(_ data: KeyedDecodingContainer<CodingKeys>, type: T.Type, forKey: CodingKeys, empty: T) -> T {
            do {
                return try data.decode(T.self, forKey: forKey)
            } catch {
                return empty
            }
        }
    }
    

    let json = Data("""
    [
        {
            "firstName": "Jon",
            "lastName": "Dough"
       },
        {
            "firstName": "Jane",
            "lastName": "Dough"
        }
    ]
    """.utf8)
    
    let decoder = JSONDecoder()
    func doDecode() -> [Char] {
        do {
            return try decoder.decode([Char].self, from: json)
        } catch {
            print("Error")
        }
        return []
    }
    
    let cast = doDecode()
    
    print("# Cs \(cast.count)")