在json Swift中从dynamic key获取值

mccptt67  于 6个月前  发布在  Swift
关注(0)|答案(2)|浏览(70)

假设我有下面的struct和json数据。我们可以写一个init(decode:Decoder)函数来转换具有此条件的对象吗?如果OptionContainer的randomKey存在,使用randomKey值来查找选项的id。如果randomKey不存在,则只需使用id。
例如,下面两个选项的id都是["1","2","3"]

My Struct
struct OptionContainer: Decodable {
    let randomKey: String?
    let id: String
    let text: String

    struct Option: Decodable {
        let id: String
        let text: String
    }
}

Json Data

[
    {
        "id": "123",
        "text": "text",
        "randomKey": "level",
        "options": [
            {
                "text": "Hello",
                "level": "1"
            },
            {
                "text": "Hello2",
                "level": "2"
            },
            {
                "text": "Hello3",
                "level": "3"
            },
        ]
    },
    {
        "id": "222",
        "text": "text2",
        "options": [
            {
                "text": "Hello",
                "id": "1"
            },
            {
                "text": "Hello2",
                "id": "2"
            },
            {
                "text": "Hello3",
                "id": "3"
            },
        ]
    },
]

字符串

4nkexdtk

4nkexdtk1#

一个可能的解决方案是将options解码为[String:String]字典,获取随机密钥(如果不存在,则将其设置为id)并手动创建Option示例。

struct OptionContainer: Decodable {
    let id: String
    let text: String
    let options : [Option]
    
    private enum CodingKeys : String, CodingKey {
        case randomKey, id, text, options
    }

    struct Option {
        let id: String
        let text: String
    }

    init(from decoder: Decoder) throws {
        let container = try decoder.container(keyedBy: CodingKeys.self)
        self.id = try container.decode(String.self, forKey: .id)
        self.text = try container.decode(String.self, forKey: .text)
        let optionData = try container.decode([[String:String]].self, forKey: .options)
        let randomKey = try container.decodeIfPresent(String.self, forKey: .randomKey) ?? "id"
        self.options = optionData.compactMap{ dict in
            guard let id = dict[randomKey], let text = dict["text"] else { return nil }
            return Option(id: id, text: text)
        }
    }
}

字符串

j2cgzkjk

j2cgzkjk2#

因为你想要一个通用的字符串键,你需要一个特殊的CodingKey:

public struct AnyCodingKey: CodingKey, CustomStringConvertible, ExpressibleByStringLiteral,
                            ExpressibleByIntegerLiteral, Hashable, Comparable {
    public var description: String { stringValue }
    public let stringValue: String
    public init(_ string: String) { self.stringValue = string }
    public init?(stringValue: String) { self.init(stringValue) }
    public var intValue: Int?
    public init(intValue: Int) {
        self.stringValue = "\(intValue)"
        self.intValue = intValue
    }
    public init(stringLiteral value: String) { self.init(value) }
    public init(integerLiteral value: Int) { self.init(intValue: value) }
    public static func < (lhs: AnyCodingKey, rhs: AnyCodingKey) -> Bool {
        lhs.stringValue < rhs.stringValue
    }
}

字符串
这只是一个样板文件。我经常使用它,我有一个片段。它只是一个CodingKey,可以是任何String或Int。
接下来,你需要一个特殊的Option初始化。它实际上不是Decodable的。它只是有一个初始化,需要一个Decoder:

struct Option {
    let id: String
    let text: String

    init(from decoder: Decoder, idKey: AnyCodingKey) throws {
        let container: KeyedDecodingContainer = try decoder.container(keyedBy: AnyCodingKey.self)
        self.id = try container.decode(String.self, forKey: idKey)
        self.text = try container.decode(String.self, forKey: "text")
    }
}


这允许您传递要用于id的密钥。
有了这个,你可以编写OptionContainer Decodable init:

init(from decoder: Decoder) throws {
    // Open up the container
    let container: KeyedDecodingContainer = try decoder.container(keyedBy: AnyCodingKey.self)

    // Decode the basic stuff
    self.id = try container.decode(String.self, forKey: "id")
    self.text = try container.decode(String.self, forKey: "text")

    // Now get the randomKey. I'm assuming you don't actually want to store it, but if you do, you can
    // If randomKey is not present, use "id". But if randomKey fails to decode (say it's an integer), throw.
    let idKey = AnyCodingKey(try container.decodeIfPresent(String.self, forKey: "randomKey") ?? "id")

    // Decode each option, passing the id key.
    var optionContainer = try container.nestedUnkeyedContainer(forKey: "options")
    var options: [Option] = []
    while !optionContainer.isAtEnd {
        // A "superDecoder" is just a new decoder that starts at this location.
        options.append(try Option(from: optionContainer.superDecoder(), idKey: idKey))
    }
    self.options = options
}


所有代码在一起:

public struct AnyCodingKey: CodingKey, CustomStringConvertible, ExpressibleByStringLiteral,
                            ExpressibleByIntegerLiteral, Hashable, Comparable {
    public var description: String { stringValue }
    public let stringValue: String
    public init(_ string: String) { self.stringValue = string }
    public init?(stringValue: String) { self.init(stringValue) }
    public var intValue: Int?
    public init(intValue: Int) {
        self.stringValue = "\(intValue)"
        self.intValue = intValue
    }
    public init(stringLiteral value: String) { self.init(value) }
    public init(integerLiteral value: Int) { self.init(intValue: value) }
    public static func < (lhs: AnyCodingKey, rhs: AnyCodingKey) -> Bool {
        lhs.stringValue < rhs.stringValue
    }
}

struct OptionContainer: Decodable {
    let id: String
    let text: String
    let options: [Option]

    struct Option {
        let id: String
        let text: String

        init(from container: KeyedDecodingContainer<AnyCodingKey>, idKey: AnyCodingKey) throws {
            self.id = try container.decode(String.self, forKey: idKey)
            self.text = try container.decode(String.self, forKey: "text")
        }
    }

    init(from decoder: Decoder) throws {
        // Open up the container
        let container: KeyedDecodingContainer = try decoder.container(keyedBy: AnyCodingKey.self)

        // Decode the basic stuff
        self.id = try container.decode(String.self, forKey: "id")
        self.text = try container.decode(String.self, forKey: "text")

        // Now get the randomKey. I'm assuming you don't actually want to store it, but if you do, you can
        // If randomKey is not present, use "id". But if randomKey fails to decode (say it's an integer), throw.
        let idKey = AnyCodingKey(try container.decodeIfPresent(String.self, forKey: "randomKey") ?? "id")

        // Decode each option, passing the id key.
        var optionContainer = try container.nestedUnkeyedContainer(forKey: "options")
        var options: [Option] = []
        while !optionContainer.isAtEnd {
            // A "superDecoder" is just a new decoder that starts at this location.
            options.append(try Option(from: optionContainer.nestedContainer(keyedBy: AnyCodingKey.self),
                                      idKey: idKey))
        }
        self.options = options
    }
}

let values = try JSONDecoder().decode([OptionContainer].self, from: json)
dump(values)

▿ 2 elements
  ▿ __lldb_expr_77.OptionContainer
    - id: "123"
    - text: "text"
    ▿ options: 3 elements
      ▿ __lldb_expr_77.OptionContainer.Option
        - id: "1"
        - text: "Hello"
      ▿ __lldb_expr_77.OptionContainer.Option
        - id: "2"
        - text: "Hello2"
      ▿ __lldb_expr_77.OptionContainer.Option
        - id: "3"
        - text: "Hello3"
  ▿ __lldb_expr_77.OptionContainer
    - id: "222"
    - text: "text2"
    ▿ options: 3 elements
      ▿ __lldb_expr_77.OptionContainer.Option
        - id: "1"
        - text: "Hello"
      ▿ __lldb_expr_77.OptionContainer.Option
        - id: "2"
        - text: "Hello2"
      ▿ __lldb_expr_77.OptionContainer.Option
        - id: "3"
        - text: "Hello3"

相关问题