Swift 4 JSON Decodable simplest way to decode type change

后端 未结 8 2009
天涯浪人
天涯浪人 2020-12-01 05:47

With Swift 4\'s Codable protocol there\'s a great level of under the hood date and data conversion strategies.

Given the JSON:

{
    \"name\": \"Bob\         


        
相关标签:
8条回答
  • 2020-12-01 05:59

    I know that this is a really late answer, but I started working on Codable couple of days back only. And I bumped into a similar issue.

    In order to convert the string to floating number, you can write an extension to KeyedDecodingContainer and call the method in the extension from init(from decoder: Decoder){}

    For the problem mentioned in this issue, see the extension I wrote below;

    extension KeyedDecodingContainer {
    
        func decodeIfPresent(_ type: Float.Type, forKey key: K, transformFrom: String.Type) throws -> Float? {
    
            guard let value = try decodeIfPresent(transformFrom, forKey: key) else {
                return nil
            }
            return Float(value)
        }
    
        func decode(_ type: Float.Type, forKey key: K, transformFrom: String.Type) throws -> Float {
    
            guard let valueAsString = try? decode(transformFrom, forKey: key),
                let value = Float(valueAsString) else {
    
                throw DecodingError.typeMismatch(
                    type, 
                    DecodingError.Context(
                        codingPath: codingPath, 
                        debugDescription: "Decoding of \(type) from \(transformFrom) failed"
                    )
                )
            }
            return value
        }
    }
    

    You can call this method from init(from decoder: Decoder) method. See an example below;

    init(from decoder: Decoder) throws {
    
        let container = try decoder.container(keyedBy: CodingKeys.self)
    
        taxRate = try container.decodeIfPresent(Float.self, forKey: .taxRate, transformFrom: String.self)
    }
    

    In fact, you can use this approach to convert any type of data to any other type. You can convert string to Date, string to bool, string to float, float to int etc.

    Actually to convert a string to Date object, I will prefer this approach over JSONEncoder().dateEncodingStrategy because if you write it properly, you can include different date formats in the same response.

    Hope I helped.

    Updated the decode method to return non-optional on suggestion from @Neil.

    0 讨论(0)
  • 2020-12-01 06:04

    enter link description hereHow to used JSONDecodable in Swift4
    1)get the JSON Response and Create Struct 2)conform Decodable class in Struct 3) Other steps in following Project(Simple Example)

    0 讨论(0)
  • 2020-12-01 06:05

    The options above only deal with the situation that the given field is always String. Many times I've met APIs where the output was once a string, other times number. So this is my suggestion to solve this. It is up to you to alter this to throw exception or set the decoded value to nil.

    var json = """
    {
    "title": "Apple",
    "id": "20"
    }
    """;
    var jsonWithInt = """
    {
    "title": "Apple",
    "id": 20
    }
    """;
    
    struct DecodableNumberFromStringToo<T: LosslessStringConvertible & Decodable & Numeric>: Decodable {
        var value: T
        init(from decoder: Decoder) {
            print("Decoding")
            if let container = try? decoder.singleValueContainer() {
                if let val = try? container.decode(T.self) {
                    value = val
                    return
                }
    
                if let str = try? container.decode(String.self) {
                    value = T.init(str) ?? T.zero
                    return
                }
    
            }
            value = T.zero
        }
    }
    
    
    struct MyData: Decodable {
        let title: String
        let _id: DecodableNumberFromStringToo<Int>
    
        enum CodingKeys: String, CodingKey {
            case title, _id = "id"
        }
    
        var id: Int {
            return _id.value
        }
    }
    
    do {
        let parsedJson = try JSONDecoder().decode(MyData.self, from: json.data(using: .utf8)!)
    
        print(parsedJson.id)
    
    } catch {
        print(error as? DecodingError)
    }
    
    
    do {
        let parsedJson = try JSONDecoder().decode(MyData.self, from: jsonWithInt.data(using: .utf8)!)
    
        print(parsedJson.id)
    
    } catch {
        print(error as? DecodingError)
    }
    
    0 讨论(0)
  • 2020-12-01 06:07

    You can always decode manually. So, given:

    {
        "name": "Bob",
        "age": 25,
        "tax_rate": "4.25"
    }
    

    You can do:

    struct Example: Codable {
        let name: String
        let age: Int
        let taxRate: Float
    
        init(from decoder: Decoder) throws {
            let values = try decoder.container(keyedBy: CodingKeys.self)
            name = try values.decode(String.self, forKey: .name)
            age = try values.decode(Int.self, forKey: .age)
            guard let rate = try Float(values.decode(String.self, forKey: .taxRate)) else {
                throw DecodingError.dataCorrupted(.init(codingPath: [CodingKeys.taxRate], debugDescription: "Expecting string representation of Float"))
            }
            taxRate = rate
        }
    
        enum CodingKeys: String, CodingKey {
            case name, age
            case taxRate = "tax_rate"
        }
    }
    

    See Encode and Decode Manually in Encoding and Decoding Custom Types.

    But I agree, that it seems like there should be a more elegant string conversion process equivalent to DateDecodingStrategy given how many JSON sources out there incorrectly return numeric values as strings.

    0 讨论(0)
  • 2020-12-01 06:09

    Unfortunately, I don't believe such an option exists in the current JSONDecoder API. There only exists an option in order to convert exceptional floating-point values to and from a string representation.

    Another possible solution to decoding manually is to define a Codable wrapper type for any LosslessStringConvertible that can encode to and decode from its String representation:

    struct StringCodableMap<Decoded : LosslessStringConvertible> : Codable {
    
        var decoded: Decoded
    
        init(_ decoded: Decoded) {
            self.decoded = decoded
        }
    
        init(from decoder: Decoder) throws {
    
            let container = try decoder.singleValueContainer()
            let decodedString = try container.decode(String.self)
    
            guard let decoded = Decoded(decodedString) else {
                throw DecodingError.dataCorruptedError(
                    in: container, debugDescription: """
                    The string \(decodedString) is not representable as a \(Decoded.self)
                    """
                )
            }
    
            self.decoded = decoded
        }
    
        func encode(to encoder: Encoder) throws {
            var container = encoder.singleValueContainer()
            try container.encode(decoded.description)
        }
    }
    

    Then you can just have a property of this type and use the auto-generated Codable conformance:

    struct Example : Codable {
    
        var name: String
        var age: Int
        var taxRate: StringCodableMap<Float>
    
        private enum CodingKeys: String, CodingKey {
            case name, age
            case taxRate = "tax_rate"
        }
    }
    

    Although unfortunately, now you have to talk in terms of taxRate.decoded in order to interact with the Float value.

    However you could always define a simple forwarding computed property in order to alleviate this:

    struct Example : Codable {
    
        var name: String
        var age: Int
    
        private var _taxRate: StringCodableMap<Float>
    
        var taxRate: Float {
            get { return _taxRate.decoded }
            set { _taxRate.decoded = newValue }
        }
    
        private enum CodingKeys: String, CodingKey {
            case name, age
            case _taxRate = "tax_rate"
        }
    }
    

    Although this still isn't as a slick as it really should be – hopefully a later version of the JSONDecoder API will include more custom decoding options, or else have the ability to express type conversions within the Codable API itself.

    However one advantage of creating the wrapper type is that it can also be used in order to make manual decoding and encoding simpler. For example, with manual decoding:

    struct Example : Decodable {
    
        var name: String
        var age: Int
        var taxRate: Float
    
        private enum CodingKeys: String, CodingKey {
            case name, age
            case taxRate = "tax_rate"
        }
    
        init(from decoder: Decoder) throws {
            let container = try decoder.container(keyedBy: CodingKeys.self)
    
            self.name = try container.decode(String.self, forKey: .name)
            self.age = try container.decode(Int.self, forKey: .age)
            self.taxRate = try container.decode(StringCodableMap<Float>.self,
                                                forKey: .taxRate).decoded
        }
    }
    
    0 讨论(0)
  • 2020-12-01 06:12

    You can use lazy var to convert the property to another type:

    struct ExampleJson: Decodable {
        var name: String
        var age: Int
        lazy var taxRate: Float = {
            Float(self.tax_rate)!
        }()
    
        private var tax_rate: String
    }
    

    One disadvantage of this approach is that you cannot define a let constant if you want to access taxRate, since the first time you access it, you are mutating the struct.

    // Cannot use `let` here
    var example = try! JSONDecoder().decode(ExampleJson.self, from: data)
    
    0 讨论(0)
提交回复
热议问题