swiftdecodable

JSON array to properties


I'm trying to map the following JSON

{
    "items": [
        {
            "id": 4,
            "name": "Caffè",
            "is_active": true,
            "position": 5,
            "include_in_menu": true,
            "custom_attributes": [
                {
                    "attribute_code": "meta_title",
                    "value": "Caffè: scopri la linea completa online"
                },
                {
                    "attribute_code": "meta_description",
                    "value": "Scopri il caffè Hausbrandt nello shop ufficiale"
                }
            ]
        },
        {
            "id": 11,
            "name": "Tè",
            "is_active": true,
            "position": 15,
            "include_in_menu": true,
            "custom_attributes": [
                {
                    "attribute_code": "meta_title",
                    "value": "Acquista le Migliori Selezioni di Tè Online"
                },
                {
                    "attribute_code": "meta_description",
                    "value": "Scopri i Tè più pregiati sullo Shop Online"
                },
                {
                    "attribute_code": "thumbnail",
                    "value": "/shop/media/catalog/category/te01.jpg"
                }
            ]
        },
        {
            "id": 17,
            "name": "Tazzine",
            "is_active": true,
            "position": 14,
            "include_in_menu": true,
            "custom_attributes": [
                {
                    "attribute_code": "meta_title",
                    "value": "Tazze e Tazzine Caffè: scopri le Collezioni"
                },
                {
                    "attribute_code": "meta_description",
                    "value": "Acquista online le Tazze e Tazzine da Caffè Hausbrandt"
                },
                {
                    "attribute_code": "thumbnail",
                    "value": "/shop/media/catalog/category/tazzine_2.jpg"
                }
            ]
        }
    ]
}

in a a struct like this:

struct ShopCategory: Decodable {

    let id: Int
    let name: String
    let position: Int
    let isActive: Bool
    let isIncluded: Bool
    let thumbnail: String?
    let metaTitle: String?
    let metaDescription: String?
}

I implemented it in this way

struct ShopCategory: Decodable {

    let id: Int
    let name: String
    let position: Int
    let isActive: Bool
    let isIncluded: Bool
    let thumbnail: String?
    let metaTitle: String?
    let metaDescription: String?

}

extension ShopCategory {

    enum CodingKeys: String, CodingKey {
        case id
        case name
        case position
        case isActive = "is_active"
        case isIncluded = "include_in_menu"
        case attributes = "custom_attributes"
    }

    init(from decoder: any Decoder) throws {
        let values = try decoder.container(keyedBy: CodingKeys.self)
        self.id = try values.decode(Int.self, forKey: .id)
        self.name = try values.decode(String.self, forKey: .name)
        self.position = try values.decode(Int.self, forKey: .position)
        self.isActive = try values.decode(Bool.self, forKey: .isActive)
        self.isIncluded = try values.decode(Bool.self, forKey: .isIncluded)
        let attributes = try values.decodeIfPresent([AttributeCode].self, forKey: .attributes)
        self.thumbnail = attributes?.first(where: { $0.code == "thumbnail" })?.value
        self.metaTitle = attributes?.first(where: { $0.code == "meta_title" })?.value
        self.metaDescription = attributes?.first(where: { $0.code == "meta_description" })?.value

    }

}

extension ShopCategory: Hashable { }

private extension ShopCategory {

    struct AttributeCode: Decodable {

        let value: String
        let code: String

        enum CodingKeys: String, CodingKey {
            case code = "attribute_code"
            case value
        }

    }

}

It works, but it doesn't like me too much. I would like to avoid searching through an array of AttributeCode. I would like to decoding a dictionary-like type and then access the attributes by key.

But I can't figure out how.


Solution

  • If you just want to avoid using first to search, you can just convert the array to a dictionary like this:

    let attributeArray = try values.decodeIfPresent([AttributeCode].self, forKey: .attributes) ?? []
    // assuming the keys are unique...
    let attributes = Dictionary(uniqueKeysWithValues: attributeArray.map { ($0.code, $0.value) })
    self.thumbnail = attributes["thumbnail"]
    self.metaTitle = attributes["meta_title"]
    self.metaDescription = attributes["meta_description"]
    

    If you want to avoid using an array at all, you can decode the JSON array as such a type:

    struct AttributeDictionary: Decodable {
        let attributes: [String: String]
        
        subscript(key: String) -> String? {
            get { attributes[key] }
        }
        
        enum CodingKeys: String, CodingKey {
            case code = "attribute_code"
            case value
        }
        
        init(from decoder: any Decoder) throws {
            // JSON arrays are decoded using an unkeyed container
            var unkeyedContainer = try decoder.unkeyedContainer()
            var dict = [String: String]()
            while !unkeyedContainer.isAtEnd {
                // go through the array and put each attribute into dict
                let attributeContainer = try unkeyedContainer.nestedContainer(keyedBy: CodingKeys.self)
                let code = try attributeContainer.decode(String.self, forKey: .code)
                let value = try attributeContainer.decode(String.self, forKey: .value)
                dict[code] = value
            }
            attributes = dict
        }
    }
    

    Usage:

    let attributes = try values.decodeIfPresent(AttributeDictionary.self, forKey: .attributes)
    self.thumbnail = attributes?["thumbnail"]
    self.metaTitle = attributes?["meta_title"]
    self.metaDescription = attributes?["meta_description"]