当属性类型可能从 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) -> Bool return lhs.userId == rhs.userId && lhs.id == rhs.id && lhs.title == rhs.title && 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<Int, String>
,如果您的模型是Decodable
,所有其他字段也是Decodable
,则不需要额外的代码。
【讨论】:
【参考方案2】:这是一个常见的IntOrString
问题。您可以将您的属性类型设为enum
,它可以处理String
或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)
由于我发现您在问题中发布的模型与您指向的 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
当postCode
为Int
时解码:
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)
当postCode
为String
时解码:
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
可以同时是String
和Int
,那么对于这个问题,您(至少)有两种可能的解决方案。首先,您可以简单地将所有邮政编码存储为String
,因为所有Int
s 都可以转换为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