当属性类型可能从Int更改为String时,如何使用Decodable协议解析JSON?

时间:2018-10-03 16:02:51

标签: swift decodable

我必须解码具有大结构和大量嵌套数组的JSON。 我已经在UserModel文件中重现了该结构,并且该结构可以工作,除了其中一个属性(邮政编码)位于嵌套数组(位置)中,该数组有时是Int,而其他属性是String。我不知道如何处理这种情况,并尝试了许多不同的解决方案。 我尝试的最后一个来自此博客https://agostini.tech/2017/11/12/swift-4-codable-in-real-life-part-2/ 它建议使用泛型。但是现在,如果不提供Decoder(),就无法初始化Location对象:

enter image description here

任何帮助或任何其他方法将不胜感激。 API调用是这样的:https://api.randomuser.me/?results=100&seed=xmoba 这是我的UserModel文件:

import Foundation
import UIKit
import ObjectMapper

struct PostModel: Equatable, Decodable{

    static func ==(lhs: PostModel, rhs: PostModel) -> Bool {
        if lhs.userId != rhs.userId {
            return false
        }
        if lhs.id != rhs.id {
            return false
        }
        if lhs.title != rhs.title {
            return false
        }
        if lhs.body != rhs.body {
            return false
        }
        return true
    }


    var userId : Int
    var id : Int
    var title : String
    var body : String

    enum key : CodingKey {
        case userId
        case id
        case title
        case body
    }

    init(from decoder: Decoder) throws {
        let container = try decoder.container(keyedBy: key.self)
        let userId = try container.decode(Int.self, forKey: .userId)
        let id = try container.decode(Int.self, forKey: .id)
        let title = try container.decode(String.self, forKey: .title)
        let body = try container.decode(String.self, forKey: .body)

        self.init(userId: userId, id: id, title: title, body: body)
    }

    init(userId : Int, id : Int, title : String, body : String) {
        self.userId = userId
        self.id = id
        self.title = title
        self.body = body
    }
    init?(map: Map){
        self.id = 0
        self.title = ""
        self.body = ""
        self.userId = 0
    }
}

extension PostModel: Mappable {



    mutating func mapping(map: Map) {
        id       <- map["id"]
        title     <- map["title"]
        body     <- map["body"]
        userId     <- map["userId"]
    }

}

4 个答案:

答案 0 :(得分:1)

您可以像这样使用泛型:

enum Either<L, R> {
    case left(L)
    case right(R)
}

extension Either: Decodable where L: Decodable, R: Decodable {
    init(from decoder: Decoder) throws {
        let container = try decoder.singleValueContainer()
        if let left = try? container.decode(L.self) {
            self = .left(left)
        } else if let right = try? container.decode(R.self) {
            self = .right(right)
        } else {
            throw DecodingError.typeMismatch(Either<L, R>.self, .init(codingPath: decoder.codingPath, debugDescription: "Expected either `\(L.self)` or `\(R.self)`"))
        }
    }
}

extension Either: Encodable where L: Encodable, R: Encodable {
    func encode(to encoder: Encoder) throws {
        var container = encoder.singleValueContainer()
        switch self {
        case let .left(left):
            try container.encode(left)
        case let .right(right):
            try container.encode(right)
        }
    }
}

然后声明postcode: Either<Int, String>,如果您的模型为Decodable,而所有其他字段均为Decodable,则不需要额外的代码。

答案 1 :(得分:0)

如果postcode既可以是String也可以是Int,那么您(至少)有两种可能的解决方案。首先,您可以简单地将所有邮政编码存储为String,因为所有Int都可以转换为String。这似乎是最好的解决方案,因为您几乎不需要对邮政编码执行任何数字运算,尤其是在某些邮政编码可能为String的情况下。另一种解决方案是为邮政编码创建两个属性,一个属性为String?,一个属性为Int?,并且始终仅根据输入数据填充两个属性之一,如Using codable with key that is sometimes an Int and other times a String中所述

该解决方案将所有邮政编码存储为String

struct PostModel: Equatable, Decodable {
    static func ==(lhs: PostModel, rhs: PostModel) -> Bool {
        return lhs.userId == rhs.userId && lhs.id == rhs.id && lhs.title == rhs.title && lhs.body == rhs.body
    }

    var userId: Int
    var id: Int
    var title: String
    var body: String
    var postcode: String

    enum CodingKeys: String, CodingKey {
        case userId, id, title, body, postcode
    }

    init(from decoder: Decoder) throws {
        let container = try decoder.container(keyedBy: CodingKeys.self)
        self.userId = try container.decode(Int.self, forKey: .userId)
        self.id = try container.decode(Int.self, forKey: .id)
        self.title = try container.decode(String.self, forKey: .title)
        self.body = try container.decode(String.self, forKey: .body)
        if let postcode = try? container.decode(String.self, forKey: .postcode) {
            self.postcode = postcode
        } else {
            let numericPostcode = try container.decode(Int.self, forKey: .postcode)
            self.postcode = "\(numericPostcode)"
        }
    }
}

答案 2 :(得分:0)

嗯,这是一个常见的cmd问题。您只需将您的媒体资源类型设为可以处理IntOrStringenum的{​​{1}}。

String

由于我发现您在问题中发布的模型与您指向的API端点中的模型不匹配,因此我创建了自己的模型和需要解码的JSON。

Int

enum IntOrString: Codable { case int(Int) case string(String) init(from decoder: Decoder) throws { let container = try decoder.singleValueContainer() do { self = try .int(container.decode(Int.self)) } catch DecodingError.typeMismatch { do { self = try .string(container.decode(String.self)) } catch DecodingError.typeMismatch { throw DecodingError.typeMismatch(IntOrString.self, DecodingError.Context(codingPath: decoder.codingPath, debugDescription: "Encoded payload conflicts with expected type, (Int or String)")) } } } func encode(to encoder: Encoder) throws { var container = encoder.singleValueContainer() switch self { case .int(let int): try container.encode(int) case .string(let string): try container.encode(string) } } } struct PostModel: Decodable { let userId: Int let id: Int let title: String let body: String let postCode: IntOrString // you don't need to implement init(from decoder: Decoder) throws // because all the properties are already Decodable } 时的解码:

postCode

Intlet jsonData = """ { "userId": 123, "id": 1, "title": "Title", "body": "Body", "postCode": 9999 } """.data(using: .utf8)! do { let postModel = try JSONDecoder().decode(PostModel.self, from: jsonData) if case .int(let int) = postModel.postCode { print(int) // prints 9999 } else if case .string(let string) = postModel.postCode { print(string) } } catch { print(error) } 时的解码:

postCode

答案 3 :(得分:0)

尝试此扩展程序

extension KeyedDecodingContainer{
    public func decodeIfPresent(_ type: String.Type, forKey key: KeyedDecodingContainer<K>.Key) throws -> String?{
        if let resStr = try? decode(type, forKey: key){
            return resStr
        }else{
            if let resInt = try? decode(Int.self, forKey: key){
                return String(resInt)
            }
            return nil
        }
    }

    public func decodeIfPresent(_ type: Int.Type, forKey key: KeyedDecodingContainer<K>.Key) throws -> Int?{
        if let resInt = try? decode(type, forKey: key){
            return resInt
        }else{
            if let resStr = try? decode(String.self, forKey: key){
                return Int(resStr)
            }
            return nil
        }
    }
}

示例

struct Foo:Codable{
    let strValue:String?
    let intValue:Int?
}

let data = """
{
"strValue": 1,
"intValue": "1"
}
""".data(using: .utf8)
print(try? JSONDecoder().decode(Foo.self, from: data!))

它将显示“ Foo(strValue:Optional(“ 1”),intValue:Optional(1))”