简体   繁体   中英

Swift 4 Codable: Converting JSON return String to Int/Date/Float

I'm going through some projects and removing JSON parsing frameworks, as it seems pretty simple to do with Swift 4. I've encountered this oddball JSON return where Ints and Dates are returned as Strings .

I looked at GrokSwift's Parsing JSON with Swift 4 , Apple's website , but I don't see anything that jumps out re: changing types.

Apple's example code shows how to change key names, but I'm having a hard time figuring out how to change the key type.

Here's what it looks like:

{
    "WaitTimes": [
        {
            "CheckpointIndex": "1",
            "WaitTime": "1",
            "Created_Datetime": "10/17/2017 6:57:29 PM"
        },
        {
            "CheckpointIndex": "2",
            "WaitTime": "6",
            "Created_Datetime": "10/12/2017 12:28:47 PM"
        },
        {
            "CheckpointIndex": "0",
            "WaitTime": "8",
            "Created_Datetime": "9/26/2017 5:04:42 AM"
        }
    ]
}

I've used CodingKey to rename dictionary keys to a Swift-conforming entry, as follows:

struct WaitTimeContainer: Codable {
  let waitTimes: [WaitTime]

  private enum CodingKeys: String, CodingKey {
    case waitTimes = "WaitTimes"
  }

  struct WaitTime: Codable {
    let checkpointIndex: String
    let waitTime: String
    let createdDateTime: String

    private enum CodingKeys: String, CodingKey {
      case checkpointIndex = "CheckpointIndex"
      case waitTime = "WaitTime"
      case createdDateTime = "Created_Datetime"
    }
  }
}

That still leaves me with String that should be Int or Date . How would I go about converting a JSON return that contains an Int/Date/Float as a String to an Int/Date/Float using the Codable protocol?

This is not yet possible as Swift team has provided only String to date decoder in JSONDecoder.

You can always decode manually though:

struct WaitTimeContainer: Decodable {
    let waitTimes: [WaitTime]

    private enum CodingKeys: String, CodingKey {
        case waitTimes = "WaitTimes"
    }

    struct WaitTime:Decodable {
        let checkpointIndex: Int
        let waitTime: Float
        let createdDateTime: Date

        init(checkpointIndex: Int, waitTime: Float, createdDateTime:Date) {
            self.checkpointIndex = checkpointIndex
            self.waitTime = waitTime
            self.createdDateTime = createdDateTime
        }

        static let formatter: DateFormatter = {
            let formatter = DateFormatter()
            formatter.calendar = Calendar(identifier: .iso8601)
            formatter.locale = Locale(identifier: "en_US_POSIX")
            formatter.timeZone = TimeZone(secondsFromGMT: 0)
            formatter.dateFormat = "MM/dd/yyyy hh:mm:ss a"
            return formatter
        }()

        init(from decoder: Decoder) throws {
            let container = try decoder.container(keyedBy: CodingKeys.self)
            let checkpointIndexString = try container.decode(String.self, forKey: .checkpointIndex)
            let checkpointIndex = Int(checkpointIndexString)!

            let waitTimeString = try container.decode(String.self, forKey: .waitTime)
            let waitTime = Float(waitTimeString)!

            let createdDateTimeString =  try container.decode(String.self, forKey: .createdDateTime)

            let createdDateTime = WaitTime.formatter.date(from: createdDateTimeString)!

            self.init(checkpointIndex:checkpointIndex, waitTime:waitTime, createdDateTime:createdDateTime)
        }

        private enum CodingKeys: String, CodingKey {
            case checkpointIndex = "CheckpointIndex"
            case waitTime = "WaitTime"
            case createdDateTime = "Created_Datetime"
        }
    }
}
public extension KeyedDecodingContainer {
public func decode(_ type: Date.Type, forKey key: Key) throws -> Date {
    let dateString = try self.decode(String.self, forKey: key)
    let dateFormatter = DateFormatter()
    dateFormatter.dateFormat = "MM/dd/yyyy hh:mm:ss a"
    guard let date = dateFormatter.date(from: dateString) else {
        let context = DecodingError.Context(codingPath: codingPath,
                                            debugDescription: "Could not parse json key to a Date")
        throw DecodingError.dataCorrupted(context)
    }
    return date
}
}

Usage: -

let date: Date = try container.decode(Date.self, forKey: . createdDateTime)

Let me give suggest two approaches: one for dealing with String backed values and another - for dealing with dates that might come in different formats. Hope the example is self-explantory.

import Foundation

protocol StringRepresentable: CustomStringConvertible {
    init?(_ string: String)
}

extension Int: StringRepresentable {}
extension Double: StringRepresentable {}

struct StringBacked<Value: StringRepresentable>: Codable, CustomStringConvertible {
    var value: Value
    
    var description: String {
        value.description
    }
    
    init(from decoder: Decoder) throws {
        let container = try decoder.singleValueContainer()
        let string = try container.decode(String.self)
        
        guard let value = Value(string) else {
            throw DecodingError.dataCorruptedError(
                in: container,
                debugDescription: """
                Failed to convert an instance of \(Value.self) from "\(string)"
                """
            )
        }
        
        self.value = value
    }
    
    func encode(to encoder: Encoder) throws {
        var container = encoder.singleValueContainer()
        try container.encode(value.description)
    }
}

let decoder = JSONDecoder()

decoder.dateDecodingStrategy = .custom({ (decoder) -> Date in
    let container = try decoder.singleValueContainer()
    let dateStr = try container.decode(String.self)
    
    let formatters = [
        "yyyy-MM-dd",
        "yyyy-MM-dd'T'HH:mm:ssZZZZZ",
        "yyyy-MM-dd'T'HH:mm:ss.SSSZZZZZ",
        "yyyy-MM-dd'T'HH:mm:ss'Z'",
        "yyyy-MM-dd'T'HH:mm:ss.SSS",
        "yyyy-MM-dd'T'HH:mm:ss.SSS'Z'",
        "yyyy-MM-dd HH:mm:ss",
        "MM/dd/yyyy HH:mm:ss",
        "MM/dd/yyyy hh:mm:ss a"
    ].map { (format: String) -> DateFormatter in
        let formatter = DateFormatter()
        formatter.locale = Locale(identifier: "en_US_POSIX")
        formatter.dateFormat = format
        return formatter
    }
    
    for formatter in formatters {
        
        if let date = formatter.date(from: dateStr) {
            return date
        }
    }
    
    throw DecodingError.valueNotFound(String.self, DecodingError.Context(codingPath: decoder.codingPath, debugDescription: "Could not parse json key: \(container.codingPath), value: \(dateStr) into a Date"))
})

// Test it with data:

let jsonData = """
{
    "WaitTimes": [
        {
            "CheckpointIndex": "1",
            "WaitTime": "1",
            "Created_Datetime": "10/17/2017 6:57:29 PM"
        },
        {
            "CheckpointIndex": "2",
            "WaitTime": "6",
            "Created_Datetime": "10/12/2017 12:28:47 PM"
        },
        {
            "CheckpointIndex": "0",
            "WaitTime": "8",
            "Created_Datetime": "9/26/2017 5:04:42 AM"
        }
    ]
}
""".data(using: .utf8)!

struct WaitTimeContainer: Codable {
    let waitTimes: [WaitTime]
    
    private enum CodingKeys: String, CodingKey {
        case waitTimes = "WaitTimes"
    }
    
    struct WaitTime: Codable {
        
        var checkpointIndex: Int {
            get { return checkpointIndexString.value }
            set { checkpointIndexString.value = newValue }
        }
        
        var waitTime: Double {
            get { return waitTimeString.value }
            set { waitTimeString.value = newValue }
        }
        
        let createdDateTime: Date
        
        private var checkpointIndexString: StringBacked<Int>
        private var waitTimeString: StringBacked<Double>
        
        private enum CodingKeys: String, CodingKey {
            case checkpointIndexString = "CheckpointIndex"
            case waitTimeString = "WaitTime"
            case createdDateTime = "Created_Datetime"
        }
    }
}

let waitTimeContainer = try decoder.decode(WaitTimeContainer.self, from: jsonData)
print(waitTimeContainer)

The technical post webpages of this site follow the CC BY-SA 4.0 protocol. If you need to reprint, please indicate the site URL or the original address.Any question please contact:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM