Swift: Codable - 提取单个编码密钥
Swift: Codable - extract a single coding key
我有以下代码来提取编码密钥中包含的 JSON:
let value = try! decoder.decode([String:Applmusic].self, from: [=11=]["applmusic"])
这成功地处理了以下 JSONs:
{
"applmusic":{
"code":"AAPL",
"quality":"good",
"line":"She told me don't worry",
}
然而,无法从以下applmusic
中提取编码密钥为applmusic
的JSON:
{
"applmusic":{
"code":"AAPL",
"quality":"good",
"line":"She told me don't worry",
},
"spotify":{
"differentcode":"SPOT",
"music_quality":"good",
"spotify_specific_code":"absent in apple"
},
"amazon":{
"amzncode":"SPOT",
"music_quality":"good",
"stanley":"absent in apple"
}
}
applmusic
、spotify
和 amazon
的数据模型不同。但是,我只需要提取 applmusic
并省略其他编码键。
我的 Swift
数据模型如下:
public struct Applmusic: Codable {
public let code: String
public let quality: String
public let line: String
}
API 以完整的 JSON 响应,我不能要求它只给我所需的字段。
如何只解码json的特定部分?似乎 Decodable
需要我先反序列化整个 json,所以我必须知道它的完整数据模型。
显然,解决方案之一是创建一个单独的 Response
模型来包含 applmusic
参数,但它看起来像一个 hack:
public struct Response: Codable {
public struct Applmusic: Codable {
public let code: String
public let quality: String
public let line: String
}
// The only parameter is `applmusic`, ignoring the other parts - works fine
public let applmusic: Applmusic
}
你能提出一个更好的方法来处理这种 JSON 结构吗?
多一点见识
我在通用扩展中使用了以下技术,它会自动为我解码 API 响应。因此,我更愿意概括一种处理此类情况的方法,而无需创建 Root
结构。如果我需要的密钥是 JSON 结构中的 3 层怎么办?
这是为我解码的扩展:
extension Endpoint where Response: Swift.Decodable {
convenience init(method: Method = .get,
path: Path,
codingKey: String? = nil,
parameters: Parameters? = nil) {
self.init(method: method, path: path, parameters: parameters, codingKey: codingKey) {
if let key = codingKey {
guard let value = try decoder.decode([String:Response].self, from: [=16=])[key] else {
throw RestClientError.valueNotFound(codingKey: key)
}
return value
}
return try decoder.decode(Response.self, from: [=16=])
}
}
}
API的定义是这样的:
extension API {
static func getMusic() -> Endpoint<[Applmusic]> {
return Endpoint(method: .get,
path: "/api/music",
codingKey: "applmusic")
}
}
您实际上不需要 Response
中的嵌套结构 Applmusic
。这将完成工作:
import Foundation
let json = """
{
"applmusic":{
"code":"AAPL",
"quality":"good",
"line":"She told me don't worry"
},
"I don't want this":"potatoe",
}
"""
public struct Applmusic: Codable {
public let code: String
public let quality: String
public let line: String
}
public struct Response: Codable {
public let applmusic: Applmusic
}
if let data = json.data(using: .utf8) {
let value = try! JSONDecoder().decode(Response.self, from: data).applmusic
print(value) // Applmusic(code: "AAPL", quality: "good", line: "She told me don\'t worry")
}
编辑:处理您的最新评论
如果 JSON 响应以嵌套 applmusic
标记的方式更改,您只需要正确更改 Response
类型。示例:
新 JSON(注意 applmusic
现在嵌套在新的 responseData
标签中):
{
"responseData":{
"applmusic":{
"code":"AAPL",
"quality":"good",
"line":"She told me don't worry"
},
"I don't want this":"potatoe",
}
}
唯一需要更改的是 Response
:
public struct Response: Decodable {
public let applmusic: Applmusic
enum CodingKeys: String, CodingKey {
case responseData
}
enum ApplmusicKey: String, CodingKey {
case applmusic
}
public init(from decoder: Decoder) throws {
let values = try decoder.container(keyedBy: CodingKeys.self)
let applmusicKey = try values.nestedContainer(keyedBy: ApplmusicKey.self, forKey: .responseData)
applmusic = try applmusicKey.decode(Applmusic.self, forKey: .applmusic)
}
}
之前的更改不会破坏任何现有代码,我们只是微调 Response
如何解析 JSON 数据以正确获取 Applmusic
目的。 JSONDecoder().decode(Response.self, from: data).applmusic
等所有调用将保持不变。
提示
最后,如果您想完全隐藏 Response
包装器逻辑,您可以使用一个 public/exposed 方法来完成所有工作;如:
// (fine-tune this method to your needs)
func decodeAppleMusic(data: Data) throws -> Applmusic {
return try JSONDecoder().decode(Response.self, from: data).applmusic
}
隐藏 Response
甚至存在的事实(使其成为 private/inaccessible),将允许您通过应用程序拥有所有代码 仅 必须打电话 decodeAppleMusic(data:)
。例如:
if let data = json.data(using: .utf8) {
let value = try! decodeAppleMusic(data: data)
print(value) // Applmusic(code: "AAPL", quality: "good", line: "She told me don\'t worry")
}
推荐阅读:
编码和解码自定义类型
更新:我对这个答案做了 JSONDecoder
的扩展,你可以在这里查看:https://github.com/aunnnn/NestedDecodable,它允许你解码任何深度的嵌套模型有一个关键路径。
你可以这样使用:
let post = try decoder.decode(Post.self, from: data, keyPath: "nested.post")
您可以制作一个 Decodable
包装器(例如,ModelResponse
此处),并放置所有逻辑以提取嵌套模型,其中包含一个键:
struct DecodingHelper {
/// Dynamic key
private struct Key: CodingKey {
let stringValue: String
init?(stringValue: String) {
self.stringValue = stringValue
self.intValue = nil
}
let intValue: Int?
init?(intValue: Int) {
return nil
}
}
/// Dummy model that handles model extracting logic from a key
private struct ModelResponse<NestedModel: Decodable>: Decodable {
let nested: NestedModel
public init(from decoder: Decoder) throws {
let key = Key(stringValue: decoder.userInfo[CodingUserInfoKey(rawValue: "my_model_key")!]! as! String)!
let values = try decoder.container(keyedBy: Key.self)
nested = try values.decode(NestedModel.self, forKey: key)
}
}
static func decode<T: Decodable>(modelType: T.Type, fromKey key: String) throws -> T {
// mock data, replace with network response
let path = Bundle.main.path(forResource: "test", ofType: "json")!
let data = try Data(contentsOf: URL(fileURLWithPath: path), options: .mappedIfSafe)
let decoder = JSONDecoder()
// ***Pass in our key through `userInfo`
decoder.userInfo[CodingUserInfoKey(rawValue: "my_model_key")!] = key
let model = try decoder.decode(ModelResponse<T>.self, from: data).nested
return model
}
}
您可以通过 JSONDecoder
(JSONDecoder
("my_model_key"
) 的 userInfo
传递您想要的密钥。然后在 ModelResponse
中转换为我们的动态 Key
以实际提取模型。
那么你可以这样使用它:
let appl = try DecodingHelper.decode(modelType: Applmusic.self, fromKey: "applmusic")
let amazon = try DecodingHelper.decode(modelType: Amazon.self, fromKey: "amazon")
let spotify = try DecodingHelper.decode(modelType: Spotify.self, fromKey: "spotify")
print(appl, amazon, spotify)
完整代码:
https://gist.github.com/aunnnn/2d6bb20b9dfab41189a2411247d04904
奖励:深度嵌套的键
经过更多尝试后,我发现您可以使用此修改后的 ModelResponse
:
轻松解码任意深度的密钥
private struct ModelResponse<NestedModel: Decodable>: Decodable {
let nested: NestedModel
public init(from decoder: Decoder) throws {
// Split nested paths with '.'
var keyPaths = (decoder.userInfo[CodingUserInfoKey(rawValue: "my_model_key")!]! as! String).split(separator: ".")
// Get last key to extract in the end
let lastKey = String(keyPaths.popLast()!)
// Loop getting container until reach final one
var targetContainer = try decoder.container(keyedBy: Key.self)
for k in keyPaths {
let key = Key(stringValue: String(k))!
targetContainer = try targetContainer.nestedContainer(keyedBy: Key.self, forKey: key)
}
nested = try targetContainer.decode(NestedModel.self, forKey: Key(stringValue: lastKey)!)
}
那么你可以这样使用它:
let deeplyNestedModel = try DecodingHelper.decode(modelType: Amazon.self, fromKey: "nest1.nest2.nest3")
从这个json:
{
"apple": { ... },
"amazon": {
"amzncode": "SPOT",
"music_quality": "good",
"stanley": "absent in apple"
},
"nest1": {
"nest2": {
"amzncode": "Nest works",
"music_quality": "Great",
"stanley": "Oh yes",
"nest3": {
"amzncode": "Nest works, again!!!",
"music_quality": "Great",
"stanley": "Oh yes"
}
}
}
}
完整代码:https://gist.github.com/aunnnn/9a6b4608ae49fe1594dbcabd9e607834
有趣的问题。我知道那是 2 周前,但我想知道
如何使用我创建的库 KeyedCodable 解决它。这是我对通用的建议:
struct Response<Type>: Codable, Keyedable where Type: Codable {
var responseObject: Type!
mutating func map(map: KeyMap) throws {
try responseObject <-> map[map.userInfo.keyPath]
}
init(from decoder: Decoder) throws {
try KeyedDecoder(with: decoder).decode(to: &self)
}
}
辅助扩展:
private let infoKey = CodingUserInfoKey(rawValue: "keyPath")!
extension Dictionary where Key == CodingUserInfoKey, Value == Any {
var keyPath: String {
set { self[infoKey] = newValue }
get {
guard let key = self[infoKey] as? String else { return "" }
return key
}
}
使用:
let decoder = JSONDecoder()
decoder.userInfo.keyPath = "applmusic"
let response = try? decoder.decode(Response<Applmusic>.self, from: jsonData)
请注意 keyPath 可能嵌套得更深我的意思是它可能是例如。 "responseData.services.applemusic"。
此外,Response 是一个 Codable,因此您无需任何额外工作即可对其进行编码。
我有以下代码来提取编码密钥中包含的 JSON:
let value = try! decoder.decode([String:Applmusic].self, from: [=11=]["applmusic"])
这成功地处理了以下 JSONs:
{
"applmusic":{
"code":"AAPL",
"quality":"good",
"line":"She told me don't worry",
}
然而,无法从以下applmusic
中提取编码密钥为applmusic
的JSON:
{
"applmusic":{
"code":"AAPL",
"quality":"good",
"line":"She told me don't worry",
},
"spotify":{
"differentcode":"SPOT",
"music_quality":"good",
"spotify_specific_code":"absent in apple"
},
"amazon":{
"amzncode":"SPOT",
"music_quality":"good",
"stanley":"absent in apple"
}
}
applmusic
、spotify
和 amazon
的数据模型不同。但是,我只需要提取 applmusic
并省略其他编码键。
我的 Swift
数据模型如下:
public struct Applmusic: Codable {
public let code: String
public let quality: String
public let line: String
}
API 以完整的 JSON 响应,我不能要求它只给我所需的字段。
如何只解码json的特定部分?似乎 Decodable
需要我先反序列化整个 json,所以我必须知道它的完整数据模型。
显然,解决方案之一是创建一个单独的 Response
模型来包含 applmusic
参数,但它看起来像一个 hack:
public struct Response: Codable {
public struct Applmusic: Codable {
public let code: String
public let quality: String
public let line: String
}
// The only parameter is `applmusic`, ignoring the other parts - works fine
public let applmusic: Applmusic
}
你能提出一个更好的方法来处理这种 JSON 结构吗?
多一点见识
我在通用扩展中使用了以下技术,它会自动为我解码 API 响应。因此,我更愿意概括一种处理此类情况的方法,而无需创建 Root
结构。如果我需要的密钥是 JSON 结构中的 3 层怎么办?
这是为我解码的扩展:
extension Endpoint where Response: Swift.Decodable {
convenience init(method: Method = .get,
path: Path,
codingKey: String? = nil,
parameters: Parameters? = nil) {
self.init(method: method, path: path, parameters: parameters, codingKey: codingKey) {
if let key = codingKey {
guard let value = try decoder.decode([String:Response].self, from: [=16=])[key] else {
throw RestClientError.valueNotFound(codingKey: key)
}
return value
}
return try decoder.decode(Response.self, from: [=16=])
}
}
}
API的定义是这样的:
extension API {
static func getMusic() -> Endpoint<[Applmusic]> {
return Endpoint(method: .get,
path: "/api/music",
codingKey: "applmusic")
}
}
您实际上不需要 Response
中的嵌套结构 Applmusic
。这将完成工作:
import Foundation
let json = """
{
"applmusic":{
"code":"AAPL",
"quality":"good",
"line":"She told me don't worry"
},
"I don't want this":"potatoe",
}
"""
public struct Applmusic: Codable {
public let code: String
public let quality: String
public let line: String
}
public struct Response: Codable {
public let applmusic: Applmusic
}
if let data = json.data(using: .utf8) {
let value = try! JSONDecoder().decode(Response.self, from: data).applmusic
print(value) // Applmusic(code: "AAPL", quality: "good", line: "She told me don\'t worry")
}
编辑:处理您的最新评论
如果 JSON 响应以嵌套 applmusic
标记的方式更改,您只需要正确更改 Response
类型。示例:
新 JSON(注意 applmusic
现在嵌套在新的 responseData
标签中):
{
"responseData":{
"applmusic":{
"code":"AAPL",
"quality":"good",
"line":"She told me don't worry"
},
"I don't want this":"potatoe",
}
}
唯一需要更改的是 Response
:
public struct Response: Decodable {
public let applmusic: Applmusic
enum CodingKeys: String, CodingKey {
case responseData
}
enum ApplmusicKey: String, CodingKey {
case applmusic
}
public init(from decoder: Decoder) throws {
let values = try decoder.container(keyedBy: CodingKeys.self)
let applmusicKey = try values.nestedContainer(keyedBy: ApplmusicKey.self, forKey: .responseData)
applmusic = try applmusicKey.decode(Applmusic.self, forKey: .applmusic)
}
}
之前的更改不会破坏任何现有代码,我们只是微调 Response
如何解析 JSON 数据以正确获取 Applmusic
目的。 JSONDecoder().decode(Response.self, from: data).applmusic
等所有调用将保持不变。
提示
最后,如果您想完全隐藏 Response
包装器逻辑,您可以使用一个 public/exposed 方法来完成所有工作;如:
// (fine-tune this method to your needs)
func decodeAppleMusic(data: Data) throws -> Applmusic {
return try JSONDecoder().decode(Response.self, from: data).applmusic
}
隐藏 Response
甚至存在的事实(使其成为 private/inaccessible),将允许您通过应用程序拥有所有代码 仅 必须打电话 decodeAppleMusic(data:)
。例如:
if let data = json.data(using: .utf8) {
let value = try! decodeAppleMusic(data: data)
print(value) // Applmusic(code: "AAPL", quality: "good", line: "She told me don\'t worry")
}
推荐阅读:
编码和解码自定义类型
更新:我对这个答案做了 JSONDecoder
的扩展,你可以在这里查看:https://github.com/aunnnn/NestedDecodable,它允许你解码任何深度的嵌套模型有一个关键路径。
你可以这样使用:
let post = try decoder.decode(Post.self, from: data, keyPath: "nested.post")
您可以制作一个 Decodable
包装器(例如,ModelResponse
此处),并放置所有逻辑以提取嵌套模型,其中包含一个键:
struct DecodingHelper {
/// Dynamic key
private struct Key: CodingKey {
let stringValue: String
init?(stringValue: String) {
self.stringValue = stringValue
self.intValue = nil
}
let intValue: Int?
init?(intValue: Int) {
return nil
}
}
/// Dummy model that handles model extracting logic from a key
private struct ModelResponse<NestedModel: Decodable>: Decodable {
let nested: NestedModel
public init(from decoder: Decoder) throws {
let key = Key(stringValue: decoder.userInfo[CodingUserInfoKey(rawValue: "my_model_key")!]! as! String)!
let values = try decoder.container(keyedBy: Key.self)
nested = try values.decode(NestedModel.self, forKey: key)
}
}
static func decode<T: Decodable>(modelType: T.Type, fromKey key: String) throws -> T {
// mock data, replace with network response
let path = Bundle.main.path(forResource: "test", ofType: "json")!
let data = try Data(contentsOf: URL(fileURLWithPath: path), options: .mappedIfSafe)
let decoder = JSONDecoder()
// ***Pass in our key through `userInfo`
decoder.userInfo[CodingUserInfoKey(rawValue: "my_model_key")!] = key
let model = try decoder.decode(ModelResponse<T>.self, from: data).nested
return model
}
}
您可以通过 JSONDecoder
(JSONDecoder
("my_model_key"
) 的 userInfo
传递您想要的密钥。然后在 ModelResponse
中转换为我们的动态 Key
以实际提取模型。
那么你可以这样使用它:
let appl = try DecodingHelper.decode(modelType: Applmusic.self, fromKey: "applmusic")
let amazon = try DecodingHelper.decode(modelType: Amazon.self, fromKey: "amazon")
let spotify = try DecodingHelper.decode(modelType: Spotify.self, fromKey: "spotify")
print(appl, amazon, spotify)
完整代码: https://gist.github.com/aunnnn/2d6bb20b9dfab41189a2411247d04904
奖励:深度嵌套的键
经过更多尝试后,我发现您可以使用此修改后的 ModelResponse
:
private struct ModelResponse<NestedModel: Decodable>: Decodable {
let nested: NestedModel
public init(from decoder: Decoder) throws {
// Split nested paths with '.'
var keyPaths = (decoder.userInfo[CodingUserInfoKey(rawValue: "my_model_key")!]! as! String).split(separator: ".")
// Get last key to extract in the end
let lastKey = String(keyPaths.popLast()!)
// Loop getting container until reach final one
var targetContainer = try decoder.container(keyedBy: Key.self)
for k in keyPaths {
let key = Key(stringValue: String(k))!
targetContainer = try targetContainer.nestedContainer(keyedBy: Key.self, forKey: key)
}
nested = try targetContainer.decode(NestedModel.self, forKey: Key(stringValue: lastKey)!)
}
那么你可以这样使用它:
let deeplyNestedModel = try DecodingHelper.decode(modelType: Amazon.self, fromKey: "nest1.nest2.nest3")
从这个json:
{
"apple": { ... },
"amazon": {
"amzncode": "SPOT",
"music_quality": "good",
"stanley": "absent in apple"
},
"nest1": {
"nest2": {
"amzncode": "Nest works",
"music_quality": "Great",
"stanley": "Oh yes",
"nest3": {
"amzncode": "Nest works, again!!!",
"music_quality": "Great",
"stanley": "Oh yes"
}
}
}
}
完整代码:https://gist.github.com/aunnnn/9a6b4608ae49fe1594dbcabd9e607834
有趣的问题。我知道那是 2 周前,但我想知道 如何使用我创建的库 KeyedCodable 解决它。这是我对通用的建议:
struct Response<Type>: Codable, Keyedable where Type: Codable {
var responseObject: Type!
mutating func map(map: KeyMap) throws {
try responseObject <-> map[map.userInfo.keyPath]
}
init(from decoder: Decoder) throws {
try KeyedDecoder(with: decoder).decode(to: &self)
}
}
辅助扩展:
private let infoKey = CodingUserInfoKey(rawValue: "keyPath")!
extension Dictionary where Key == CodingUserInfoKey, Value == Any {
var keyPath: String {
set { self[infoKey] = newValue }
get {
guard let key = self[infoKey] as? String else { return "" }
return key
}
}
使用:
let decoder = JSONDecoder()
decoder.userInfo.keyPath = "applmusic"
let response = try? decoder.decode(Response<Applmusic>.self, from: jsonData)
请注意 keyPath 可能嵌套得更深我的意思是它可能是例如。 "responseData.services.applemusic"。
此外,Response 是一个 Codable,因此您无需任何额外工作即可对其进行编码。