Search code examples
jsonswiftcodabledecodable

Swift 4 Decodable: struct from nested array


Given the following JSON document I'd like to create a struct with four properties: filmCount (Int), year (Int), category (String), and actor (Actor array).

{    
    "filmCount": 5,
    "year": 2018,
    "category": "Other",
    "actors":{  
        "nodes":[  
            {  
                "actor":{  
                    "id":0,
                    "name":"Daniel Craig"
                }
            },
            {  
                "actor":{  
                    "id":1,
                    "name":"Naomie Harris"
                }
            },
            {  
                "actor":{  
                    "id":2,
                    "name":"Rowan Atkinson"
                }
            }
        ]
    }
}

PlacerholderData is a struct storing the three main properties and the list of actors which should be retrieved from the nested nodes container within the actors property from the JSON object.

PlacerholderData:

struct PlaceholderData: Codable {
    let filmCount: Int
    let year: Int
    let category: String
    let actors: [Actor]
}

Actor.swift:

struct Actor: Codable {
    let id: Int
    let name: String
}

I am attempting to do this through providing my own init to initialise the values from the decoder's container manually. How can I go about fixing this without having to have an intermediate struct storing a nodes object?


Solution

  • You can use nestedContainer(keyedBy:) and nestedUnkeyedContainer(forKey:) for decoding nested array and dictionary like this to turn it into your desired structure. Your decoding in init(decoder: ) might look something like this,

    Actor extension for decoding,

    extension Actor: Decodable {
    
        enum CodingKeys: CodingKey { case id, name }
    
        enum ActorKey: CodingKey { case actor }
    
        init(from decoder: Decoder) throws {
            let rootKeys        = try decoder.container(keyedBy: ActorKey.self)
            let actorContainer  = try rootKeys.nestedContainer(keyedBy: CodingKeys.self,
                                                               forKey: .actor)
            try id =  actorContainer.decode(Int.self,
                                           forKey: .id)
            try name =  actorContainer.decode(String.self,
                                             forKey: .name)
        }
    }
    

    PlaceholderData extension for decoding,

    extension PlaceholderData: Decodable {
    
        enum CodingKeys: CodingKey { case filmCount, year, category, actors }
    
        enum NodeKeys: CodingKey { case nodes }
    
        init(from decoder: Decoder) throws {
            let rootContainer   = try decoder.container(keyedBy: CodingKeys.self)
            try filmCount       =  rootContainer.decode(Int.self,
                                                        forKey: .filmCount)
            try year            =  rootContainer.decode(Int.self,
                                                        forKey: .year)
            try category        =  rootContainer.decode(String.self,
                                                        forKey: .category)
            let actorsNode      = try rootContainer.nestedContainer(keyedBy: NodeKeys.self,
                                                                    forKey: .actors)
            var nodes = try actorsNode.nestedUnkeyedContainer(forKey: .nodes)
            var allActors: [Actor] = []
    
            while !nodes.isAtEnd {
                let actor = try nodes.decode(Actor.self)
                allActors += [actor]
            }
            actors = allActors
        }
    }
    

    Then, you can decode it like this,

    let decoder = JSONDecoder()
    do {
        let placeholder = try decoder.decode(PlaceholderData.self, from: jsonData)
        print(placeholder)
    } catch {
        print(error)
    }
    

    Here, the basic idea is to decode dictionary container using nestedContainer(keyedBy:) and array container using nestedUnkeyedContainer(forKey:)