Home > Net >  Swift & Codable: how to "bypass" some JSON levels?
Swift & Codable: how to "bypass" some JSON levels?

Time:05-09

I would like to use this Pokémon API to fetch some data and convert it into a Swift Pokemon struct.

Here is an extract of the response I get when fetching Pokemon #142:

{
    "id": 142,
    "name": "aerodactyl",
    "types": [{
            "type": {
                "name": "rock",
                "url": "https://pokeapi.co/api/v2/type/6/"
            },
            "slot": 1
        },
        {
            "type": {
                "name": "flying",
                "url": "https://pokeapi.co/api/v2/type/3/"
            },
            "slot": 2
        }
    ]
}

Here is the struct I wrote to convert this JSON into a Swift type:

struct Pokemon: Codable {
    var id: Int
    let name: String
    var types: [PokemonType]?
}

struct PokemonType: Codable {
    var type: PokemonTypeContent
}

struct PokemonTypeContent: Codable {
    var name: PokemonTypeNameContent
}

enum PokemonTypeNameContent: String, Codable {
    case flying = "flying"
    case rock = "rock"
    // ...
}

Now here is my problem: when I want to get the Pokemon types, I need to dig into this:

pokemon.types.first?.type.name

I would like to know if I have instead a way of getting the PokemonTypeNameContent array in the Pokemon struct, to do something like this:

struct Pokemon {
    var types: [PokemonTypeNameContent]?
}

(I am not interested in getting the slot values).

Thank you for your help!

CodePudding user response:

You can do custom encoding for PokemonTypeNameContent, and traverse through the levels of JSON using nestedContainer

enum PokemonTypeNameContent: String, Decodable {
    case flying = "flying"
    case rock = "rock"
    // ...
    
    enum OuterCodingKeys: CodingKey { case type }
    enum InnerCodingKeys: CodingKey { case name }
    
    init(from decoder: Decoder) throws {
        // this is the container for each JSON object in the "types" array
        let container = try decoder.container(keyedBy: OuterCodingKeys.self)

        // this finds the nested container (i.e. JSON object) associated with the key "type"
        let innerContainer = try container.nestedContainer(keyedBy: InnerCodingKeys.self, forKey: .type)

        // now we can decode "name" as a string
        let name = try innerContainer.decode(String.self, forKey: .name)
        if let pokemonType = Self.init(rawValue: name) {
            self = pokemonType
        } else {
            throw DecodingError.typeMismatch(
                PokemonTypeNameContent.self,
                    .init(codingPath: innerContainer.codingPath   [InnerCodingKeys.name],
                          debugDescription: "Unknown pokemon type '\(name)'",
                          underlyingError: nil
                         )
            )
        }
    }
}

// Pokemon can then be declared like this:
struct Pokemon: Decodable {
    let id: Int
    let name: String
    let types: [PokemonTypeNameContent]
}

Do note that this means that you lose the option of decoding PokemonTypeNameContent as a regular enum. If you do want to do that, put the custom decoding code into a property wrapper. Note that we would be decoding the entire JSON array, instead of each JSON object.

@propertyWrapper
struct DecodePokemonTypes: Decodable {
    var wrappedValue: [PokemonTypeNameContent]
    
    init(wrappedValue: [PokemonTypeNameContent]) {
        self.wrappedValue = wrappedValue
    }
    
    enum OuterCodingKeys: CodingKey { case type }
    enum InnerCodingKeys: CodingKey { case name }
    
    init(from decoder: Decoder) throws {
        // container for the "types" JSON array
        var unkeyedContainer = try decoder.unkeyedContainer()
        wrappedValue = []

        // while we are not at the end of the JSON array
        while !unkeyedContainer.isAtEnd {

            // similar to the first code snippet
            let container = try unkeyedContainer.nestedContainer(keyedBy: OuterCodingKeys.self)
            let innerContainer = try container.nestedContainer(keyedBy: InnerCodingKeys.self, forKey: .type)
            let name = try innerContainer.decode(String.self, forKey: .name)
            if let pokemonType = PokemonTypeNameContent(rawValue: name) {
                wrappedValue.append(pokemonType)
            } else {
                throw DecodingError.typeMismatch(
                    PokemonTypeNameContent.self,
                        .init(codingPath: innerContainer.codingPath   [InnerCodingKeys.name],
                              debugDescription: "Unknown pokemon type '\(name)'",
                              underlyingError: nil
                             )
                )
            }
        }
    }
}

// You would write this in Pokemon
@DecodePokemonTypes
var types: [PokemonTypeNameContent]
  • Related