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

Posted

技术标签:

【中文标题】当属性类型可能从 Int 更改为 String 时,如何使用 Decodable 协议解析 JSON?【英文标题】:How to parse JSON with Decodable protocol when property types might change from Int to String? 【发布时间】:2019-03-08 22:27:43 【问题描述】:

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

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

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"]
    


【问题讨论】:

与你的问题无关,但==函数可以简化为static func ==(lhs: PostModel, rhs: PostModel) -&gt; Bool return lhs.userId == rhs.userId &amp;&amp; lhs.id == rhs.id &amp;&amp; lhs.title == rhs.title &amp;&amp; lhs.body == rhs.body 。你现在的init(from:)方法也不需要,编译器可以自动合成,你的init(userId:, id:, title:, body:)方法也是如此。 确实聊胜于无,谢谢 Using codable with key that is sometimes an Int and other times a String的可能重复 在 Swift 4.1+ 中,如果要比较所有属性,即使显式的 static == 函数也会被合成。 @Larme 不一样,这个 Json 具有嵌套数组,您访问属性的方式与您提供的重复问题不同。 【参考方案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&lt;Int, String&gt;,如果您的模型是Decodable,所有其他字段也是Decodable,则不需要额外的代码。

【讨论】:

【参考方案2】:

这是一个常见的IntOrString 问题。您可以将您的属性类型设为enum,它可以处理StringInt

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)
        
    

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

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

postCodeInt时解码:

let 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)

postCodeString时解码:

let 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)
     else if case .string(let string) = postModel.postCode 
        print(string) // prints "9999"
    
 catch 
    print(error)

【讨论】:

【参考方案3】:

如果postcode 可以同时是StringInt,那么对于这个问题,您(至少)有两种可能的解决方案。首先,您可以简单地将所有邮政编码存储为String,因为所有Ints 都可以转换为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)"
        
    

【讨论】:

不像那样工作,这不是我拥有的那种结构,我实例化 [User].self 并且在用户内部有一个嵌套的 Location() 对象,它具有属性邮政编码。所以当 init(from decoder:throws) 时,一切都发生在这一行: let users = try container.decode([User].self, forKey: .results) @user3033437 那你为什么在你的问题中包含不相关的代码?您应该使用导致问题的实际代码和一些示例 JSON 响应来编辑您的问题 除了已经发布的错误之外,我无法提供来自应用程序的 Json 示例响应。 json 永远不会形成,因为当邮政编码是 String 而不是 Int 时应用程序崩溃,反之亦然! @user3033437 您可以在解析之前轻松打印 JSON 响应,设置异常断点或简单地使用 try? 来避免崩溃以检索 JSON 响应【参考方案4】:

试试这个扩展

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))"

【讨论】:

以上是关于当属性类型可能从 Int 更改为 String 时,如何使用 Decodable 协议解析 JSON?的主要内容,如果未能解决你的问题,请参考以下文章

快速核心数据迁移以更改属性类型

如何将特定行/单元格的类型从 int 更改为 varchar

在 Pentaho 数据集成中将字段从 String 更改为 Int

ASP.NET Identity - 将用户 ID 主键默认类型从字符串更改为 int 以及使用自定义表名时出错

将带有数字的列的类型从 varchar 更改为 int

ListAdapter 在将数组类型更改为 int 后拒绝应用程序