Search code examples
iosjsonswiftdecodable

Decode nested jsons swift


Hello people I have a little question related with the Encodable protocol in Swift.

I have the following json file:

let magicJson = """
{
    "value": [
        {
        "scheduleId": "[email protected]",
        "somethingEventMoreMagical": "000220000"
        }
    ]
}
""".data(using: .utf8)!

For decoding I tried to avoid having to create two objects that both go with Decodable, and the first one has an array of the second object. I would like to flatten that object into something like this:

struct MagicalStruct: Decodable {
    private enum CodingKeys: String, CodingKey {
        case value
    }
    
    private enum ScheduleCodingKeys: String, CodingKey {
        case roomEmail = "scheduleId"
    }
    
    let roomEmail: String
    
    init(from decoder: Decoder) throws {
        let container = try decoder.container(keyedBy: CodingKeys.self)
        let magicContainer = try container.nestedContainer(keyedBy: ScheduleCodingKeys.self, forKey: .value)
        roomEmail = try magicContainer.decode(String.self, forKey: ScheduleCodingKeys.roomEmail)
    }
}

However when I try the following code: JSONDecoder().decode(MagicalStruct.self, magicJson) I get that it expects an array but gets a dictionary. On the other hand when I go with JSONDecoder().decode([MagicalStruct].self, magicJson), I get that it receives an array but expects a dictionary.

Does anyone know why this is happening ?


Solution

  • First when you're decoding your struct using:

    JSONDecoder().decode(MagicalStruct.self, magicJson)
    

    you're trying to extract a single object: let roomEmail: String.

    However, your input JSON contains an array of objects with emails. Which means your code:

    init(from decoder: Decoder) throws {
        let container = try decoder.container(keyedBy: CodingKeys.self)
        let magicContainer = try container.nestedContainer(keyedBy: ScheduleCodingKeys.self, forKey: .value)
        roomEmail = try magicContainer.decode(String.self, forKey: ScheduleCodingKeys.roomEmail)
    }
    

    tries to decode a single email and there is a collection instead (in your example with one element - that's why it may be confusing).

    Also your error Expected to decode Dictionary<String, Any> but found an array instead is on the line:

    let magicContainer = try container.nestedContainer(keyedBy: ScheduleCodingKeys.self, forKey: .value)
    

    You need to decode an array:

    var magicContainer = try container.nestedUnkeyedContainer(forKey: .value)
    

    But then you have an array of objects with scheduleId and somethingEventMoreMagical keys. How do you want to assign all the values to your one let roomEmail: String variable?


    You can decode a dictionary instead:

    let result = try JSONDecoder().decode([String: [MagicalStruct]].self, from: magicJson)
    
    print(result["value"]!) // prints [MagicalStruct(roomEmail: "[email protected]")]
    

    And you can simplify your MagicalStruct:

    struct MagicalStruct: Decodable {
        enum CodingKeys: String, CodingKey {
            case roomEmail = "scheduleId"
        }
    
        let roomEmail: String
    }