skip to Main Content

Lets say I have the following struct and json data. Can We write a init(decode: Decoder) function to covert object with this condition? If the randomKey for the OptionContainer exist, use the randomKey value to look for the id for the option. If the randomKey doesn’t exist, then simply use the id.

Example, both the ids for the two options below would be ["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"
            },
        ]
    },
]

2

Answers


  1. A possible solution is to decode options as [String:String] dictionary, get the random key (set it to id if it doesn’t exist) and create Option instances manually

    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)
            }
        }
    }
    
    Login or Signup to reply.
  2. Since you want a generic string key, you’ll need a special 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
        }
    }
    

    This is mostly just boilerplate. I use it so often I have a snippet for it. It’s just a CodingKey that can be any String or Int.

    Next, you’ll want a special init for Option. It is not actually Decodable. It just has an init that takes a 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")
        }
    }
    

    This allows you to pass the key you want to use for id.

    And with that, you can write the 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
    }
    

    All the code together:

    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"
    
    Login or Signup to reply.
Please signup or login to give your own answer.
Back To Top
Search