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.
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"]