user1118374
user1118374

Reputation: 174

Decoding JSON with Swift containing dictionary with different types

I have a JSON format that I'm trying to parse with JSONDecoder but due to the way the JSON is structured, I can't figure out how to do it.

Here's the format of the JSON. I'm going to leave out bits for brevity.

{
  "name":"John Smith",
  "addresses":[
    {
      "home":{
        "street":"123 Main St",
        "state":"CA"
      }
    },
    {
      "work":{
        "street":"345 Oak St",
        "state":"CA"
      }
    },
    {
      "favorites":[
        {
          "street":"456 Green St.",
          "state":"CA"
        },
        {
          "street":"987 Tambor Rd",
          "state":"CA"
        }
      ]
    }
  ]
}    

I don't know how to define a Decodable struct that I can then decode. addresses is an array of dictionaries. home and work each contain a single address while favorites contains an array of addresses. I can't define addresses as [Dictionary<String, Address] since favorites is an array of addresses. I can't define it as [Dictionary<String, Any>] because then I get an Type 'UserProfile' does not conform to protocol 'Encodeable' error.

Does anyone have any ideas how I can parse this? How do I parse a dictionary where the value changes depending on the key?

Thank you.

Upvotes: 1

Views: 4399

Answers (2)

Larme
Larme

Reputation: 26026

A possible solution, using an enum, which might be either a work, home or fav:

struct Top: Decodable {
    
    let name: String
    let addresses: [AddressType]
    
    enum CodingKeys: String, CodingKey {
        case name
        case addresses
    }
    
    init(from decoder: Decoder) throws {
        let container = try decoder.container(keyedBy: CodingKeys.self)
        self.name = try container.decode(String.self, forKey: .name)
        self.addresses = try container.decode([AddressType].self, forKey: .addresses)
    }
}

enum AddressType: Decodable {
    
    case home(Address)
    case work(Address)
    case favorites([Address])
    
    enum CodingKeys: String, CodingKey {
        case home
        case work
        case favorites
    }
    
    init(from decoder: Decoder) throws {
        let container = try decoder.container(keyedBy: CodingKeys.self)
        if let home = try container.decodeIfPresent(Address.self, forKey: .home) {
            self = AddressType.home(home)
        } else if let work = try container.decodeIfPresent(Address.self, forKey: .work) {
            self = AddressType.work(work)
        } else {
            let favorites = try container.decodeIfPresent([Address].self, forKey: .favorites)
            self = AddressType.favorites(favorites ?? [])
        }
    }
}

struct Address: Decodable {
    let street: String
    let state: String
}

Testing (with fixes on your JSON I guess):

let jsonStr = """
{
    "name": "John Smith",
    "addresses": [{
        "home": {
            "street": "123 Main St",
            "state": "CA"
        }
    }, {
        "work": {
            "street": "345 Oak St",
            "state": "CA"
        }
    }, {
        "favorites": [{
            "street": "456 Green St.",
            "state": "CA"
        }, {
            "street": "987 Tambor Rd",
            "state": "CA"
        }]
    }]
}
"""

let jsonData = jsonStr.data(using: .utf8)!

do {
    let top = try JSONDecoder().decode(Top.self, from: jsonData)
    
    print("Top.name: \(top.name)")
    top.addresses.forEach {
        switch $0 {
        case .home(let address):
            print("It's a home address:\n\t\(address)")
        case .work(let address):
            print("It's a work address:\n\t\(address)")
        case .favorites(let addresses):
            print("It's a favorites addresses:")
            addresses.forEach{ aSubAddress in
                print("\t\(aSubAddress)")
            }

        }
    }
} catch {
    print("Error: \(error)")
}

Output:

$>Top.name: John Smith
$>It's a home address:
    Address(street: "123 Main St", state: "CA")
$>It's a work address:
    Address(street: "345 Oak St", state: "CA")
$>It's a favorites addresses:
    Address(street: "456 Green St.", state: "CA")
    Address(street: "987 Tambor Rd", state: "CA")

Note: Afterwards, you should be able to have lazy variables on Top depending on your needs:

lazy var homeAddress: Address? = {
    return self.addresses.compactMap {
        if case AddressType.home(let address) = $0 {
            return address
        }
        return nil
    }.first
}()

Upvotes: 1

gcharita
gcharita

Reputation: 8327

I assume that your JSON is something like this:

{
  "name": "John Smith",
  "addresses": [
    {
      "home": {
        "street": "123 Main St",
        "state": "CA"
      }
    },
    {
      "work": {
        "street": "345 Oak St",
        "state": "CA"
      }
    },
    {
      "favorites": [
        {
          "street": "456 Green St.",
          "state": "CA"
        },
        {
          "street": "987 Tambor Rd",
          "state": "CA"
        }
      ]
    }
  ]
}

I had to make some changes to be a valid JSON.

You can use the following structure to always map addresses property to [[String: [Address]]]:

struct Response: Decodable {
    let name: String
    let addresses: [[String: [Address]]]
    
    enum CodingKeys: String, CodingKey {
        case name
        case addresses
    }
    
    init(from decoder: Decoder) throws {
        let container = try decoder.container(keyedBy: CodingKeys.self)
        
        name = try container.decode(String.self, forKey: .name)
        var unkeyedContainer = try container.nestedUnkeyedContainer(forKey: .addresses)
        var addresses = [[String: [Address]]]()
        while !unkeyedContainer.isAtEnd {
            do {
                let sindleAddress = try unkeyedContainer.decode([String: Address].self)
                addresses.append(sindleAddress.mapValues { [$0] } )
            } catch DecodingError.typeMismatch {
                addresses.append(try unkeyedContainer.decode([String: [Address]].self))
            }
        }
        self.addresses = addresses
    }
}

struct Address: Decodable {
    let street: String
    let state: String
}

Basically, in the custom implementation of init(from:) we try to decode addresses property to [String: Address], if that succeeds a new dictionary of type [String: [Address]] is created with only one element in the value array. If it fails then we decode addresses property to [String: [Address]].

Update: I would prefer though to add another struct:

struct AddressType {
    let label: String
    let addressList: [Address]
}

and modify the addresses property of Response to be [AddressType]:

struct Response: Decodable {
    let name: String
    let addresses: [AddressType]
    
    enum CodingKeys: String, CodingKey {
        case name
        case addresses
    }
    
    init(from decoder: Decoder) throws {
        let container = try decoder.container(keyedBy: CodingKeys.self)
        
        name = try container.decode(String.self, forKey: .name)
        var unkeyedContainer = try container.nestedUnkeyedContainer(forKey: .addresses)
        var addresses = [AddressType]()
        while !unkeyedContainer.isAtEnd {
            let addressTypes: [AddressType]
            do {
                addressTypes = try unkeyedContainer.decode([String: Address].self).map {
                    AddressType(label: $0.key, addressList: [$0.value])
                }
            } catch DecodingError.typeMismatch {
                addressTypes = try unkeyedContainer.decode([String: [Address]].self).map {
                    AddressType(label: $0.key, addressList: $0.value)
                }
            }
            addresses.append(contentsOf: addressTypes)
        }
        self.addresses = addresses
    }
}

Upvotes: 3

Related Questions