0

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!

2
  • 2
    You can write a custom init method and decode the subcontainer with nestedContainer but this causes more effort and less efficiency than decode all levels. Commented May 8, 2022 at 17:18
  • 1
    You can indeed write a custom init(from decoder:), but it'd be simpler to add computed properties to the structure that would do get it directly. Commented May 8, 2022 at 18:03

1 Answer 1

1

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]
Sign up to request clarification or add additional context in comments.

Comments

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.