Swift & Codable:如何 "bypass" 一些 JSON 级别?

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

我想使用 this Pokémon API 获取一些数据并将其转换为 Swift Pokemon 结构。

这是我在获取 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
        }
    ]
}

这是我编写的用于将此 JSON 转换为 Swift 类型的结构:

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"
    // ...
}

现在这是我的问题:当我想获得口袋妖怪类型时,我需要深入研究:

pokemon.types.first?.type.name

我想知道我是否有一种方法可以在 Pokemon 结构中获取 PokemonTypeNameContent 数组,以执行如下操作:

struct Pokemon {
    var types: [PokemonTypeNameContent]?
}

(我对获取 slot 值不感兴趣)。

感谢您的帮助!

您可以为 PokemonTypeNameContent 进行自定义编码,并使用 nestedContainer

遍历 JSON 的级别
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]
}

请注意,这意味着您失去了将 PokemonTypeNameContent 解码为常规枚举的选项。如果您确实想这样做,请将自定义解码代码放入 属性 包装器中。请注意,我们将解码整个 JSON 数组,而不是每个 JSON 对象。

@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]