Home > Blockchain >  How to map a single value from a JSON dictionary to a property when using Swift Decodable
How to map a single value from a JSON dictionary to a property when using Swift Decodable

Time:08-29

I have a JSON object that looks something like this:

{
    "name": "Acid Arrow",
    "school": {
        "name": "Evocation",
        "url": "http://www.dnd5eapi.co/api/magic-schools/5"
    }
}

that I would like to model in Swift as the following:

struct Spell: Decodeable {
    let name: String
    let school: MagicSchool
}

enum MagicSchool: String {
    case abjuration = "Abjuration"
    case abjuration = "Abjuration" 
    case conjuration = "Conjuration" 
    case divination = "Divination"   
    case enchantment = "Enchantment"    
    case evocation = "Evocation"   
    case illusion = "Illusion"   
    case necromancy = "Necromancy"  
    case transmutation = "Transmutation"   
}

The only ways I can find to reduce the JSON school dictionary down to a single enumeration value is to implement the entire Decodeable by providing a custom init(from decoder: Decoder) initializer that would look something like this:

extension Spell: Decodeable {
    init(from decoder: Decoder) {
        let values = try decoder.container(keyedBy: CodingKeys.self)

        // manually map to the spell name
        name = try values.decode(String.self, forKey: .name)

        // manually decode **school** into a dictionary
        let jsonSchool = try values.decode(Dictionary<String,String>.self, forKey: .school)

        // extract the "name" property from the dict and assign it as `MagicSchool` enum
        school = MagicSchool(rawValue: jsonSchool["name"])

    }
}

But it doesn't like it because of a type conflict on the key type for Spell.school

Am I trying to do this the wrong way? Is there a simpler way to transform a complex type into a basic type or to specify a path in the mapping?

CodePudding user response:

Using init(from decoder: Decoder) throws { initializer is the appropriate way of dealing with this scenario.

There are probably different ways of dealing with the school entity. I prefer decoding it into a struct to get type safety.

And as pointed out in the comments the code you provided is full of typos. After cleaning those up it was just a case of an optional beeing assigned to a non optional field.

struct Spell: Decodable {
    let name: String
    let school: MagicSchool
}

enum MagicSchool: String {
    case abjuration = "Abjuration"
    case conjuration = "Conjuration"
    case divination = "Divination"
    case enchantment = "Enchantment"
    case evocation = "Evocation"
    case illusion = "Illusion"
    case necromancy = "Necromancy"
    case transmutation = "Transmutation"
}

extension Spell {
    
    struct InternalSchool: Decodable{
        let name: String
        let url: String
    }
    
    enum CodingKeys: CodingKey{
        case name, school
    }
    
    init(from decoder: Decoder) throws {
        let values = try decoder.container(keyedBy: CodingKeys.self)

        // manually map to the spell name
        name = try values.decode(String.self, forKey: .name)

        // manually decode **school** into the custom type
        let school = try values.decode(InternalSchool.self, forKey: .school)

        // check if you can create an enum from the given string and throw appropriate error
        guard let schoolEnum = MagicSchool(rawValue: school.name) else{
            throw DecodingError.dataCorrupted(.init(codingPath: [CodingKeys.school], debugDescription: "school has unknown value"))
        }
        // assign enum
        self.school = schoolEnum
    }
}
  • Related