问题描述
我有以下代码来提取编码密钥中包含的JSON:
let value = try! coder.decode([String:Applmusic] .self,from:$ 0 [ applmusic])
这样可以成功处理以下JSON:
{
applmusic:{
code: AAPL,
质量:好,
行:她告诉我别担心,
}
但是,无法从以下代码中提取带有 applmusic
编码键的JSON:
{
applmusic:{
code: AAPL,
quality : good,
line:她告诉我不用担心,
},
spotify:{
differentcode: SPOT ,
music_quality:良好,
spotify_specific_code:苹果中不存在
},
amazon:{
amzncode: SPOT,
music_quality:好,
stanley:苹果中不存在
}
}
applmusic
, spotify
的数据模型和 amazon
不同。但是,我只需要提取 applmusic
并省略其他编码键即可。
我的 Swift
数据模型如下:
public struct Applmusic:可编码{
public let code :字符串
公共允许质量:字符串
公共允许行:字符串
}
API使用完整的JSON进行响应,我不能要求它仅向我提供所需的字段。
如何仅解码JSON的特定部分?看来, Decodable
要求我先反序列化整个json,所以我必须知道它的完整数据模型。
很明显,解决方案之一是创建一个单独的 Response
模型,仅包含 applmusic
参数,但看起来像是黑客:
public struct响应:可编码{
public struct Applmusic:Codable {
公共让路代码:字符串
公共让路质量:字符串
公共让路行:字符串
}
//唯一的参数是 applmusic,忽略了其他部分-作品罚款
公开让其贴身:贴身
}
您能提出一种更好的方法吗
更多见识
在通用扩展中使用以下技术为我自动解码API响应。因此,我宁愿概括一种处理此类情况的方法,而无需创建 Root
结构。如果我需要的密钥是JSON结构中的3层,该怎么办?
这是为我解码的扩展名:
扩展端点,其中响应:Swift.Decodable {
便捷初始化(方法:方法= .get,
路径:Path,
encodingKey:String?= nil,
参数:Parameters?= nil){
self.init(方法:方法,路径:路径,参数:参数,encodingKey:编码键){
如果键=编码键{
保护值=尝试解码器.decode([String:Response] .self,from:$ 0)[key] else {
throw RestClientError.valueNotFound(codingKey:key)
}
返回值
}
返回尝试encoder.decode(Response.self,from:$ 0)
}
}
}
API的定义如下:
扩展API {
static func getMusic()->端点< [Applmusic]> {
return Endpoint(方法:.get,
路径: / api / music,
encodingKey: applmusic)
}
}
已更新:我对 JSONDecoder ,您可以在此处进行检查:,它允许您使用键路径解码任何深度的嵌套模型。
您可以像这样使用它:
让post =试试解码器。解码(Post。自我,来自:数据,键路径: nested.post)
您可以制作一个 Decodable
包装器(例如,这里的 ModelResponse
),然后提取所有逻辑
struct DecodingHelper {
///动态键
私有str uct键:CodingKey {
let stringValue:字符串
init?(stringValue:String){
self.stringValue = stringValue
self.intValue = nil
}
let int值:整数?
init?(intValue:Int){
return nil
}
}
///虚拟模型处理从键$中提取模型的逻辑b $ b私有结构ModelResponse< NestedModel:Decodable> ;:可解码{{b $ b let nested:NestedModel
public init(来自解码器:Decoder)抛出{
let key = Key(stringValue :解码器.userInfo [CodingUserInfoKey(rawValue: my_model_key)!]!as!String)!
让值=尝试解码器。容器(keyedBy:Key.self)
嵌套=尝试值.decode(NestedModel.self,forKey:键)
}
}
静态函数解码< T:Decodable>(modelType:T.Type,fromKey key:String)抛出-> T {
//模拟数据,替换为网络响应
let path = Bundle.main.path(forResource: test,ofType: json)!
let data = try Data(contentsOf:URL(fileURLWithPath:path),选项:.mappedIfSafe)
let解码器= JSONDecoder()
// ** *通过`userInfo`
解码器传递用户密钥。userInfo[CodingUserInfoKey(rawValue: my_model_key)!] =密钥
让模型=尝试解码器.decode(ModelResponse< T> .self,来自:数据)。嵌套
返回模型
}
}
您可以通过 JSONDecoder
( my_model_key
)。然后将其转换为我们在 ModelResponse
内部的动态 Key
,以实际提取模型。
然后您可以像这样使用它:
let appl =尝试DecodingHelper.decode(modelType:Applmusic.self,fromKey: applmusic)
let amazon =试试DecodingHelper.decode(modelType:Amazon.self,fromKey: amazon)
let spotify =试试DecodingHelper.decode(modelType:Spotify.self,fromKey: spotify)
print(appl,amazon,spotify)
完整代码:
奖金:深度嵌套的密钥
玩了更多之后,我发现您可以使用修改后的 ModelResponse
轻松解码任意深度的键:
私有结构模型响应e< NestedModel:Decodable> ;: Decodable {
让嵌套:NestedModel
public init(来自解码器:Decoder)throw {
//用'。'$ b拆分嵌套路径$ b var keyPaths =(decoder.userInfo [CodingUserInfoKey(rawValue: my_model_key)!]!]!如! String).split(separator:。)
//获取最后要提取的键
let lastKey = String(keyPaths.popLast()!)
//循环获取容器,直到到达最后一个
var targetContainer =尝试解码器.container(keyedBy:Key.self)
for keyPaths中的k {
let key = Key(stringValue:字串(k))!
targetContainer =试试targetContainer.nestedContainer(keyedBy:Key.self,forKey:key)
}
nested =试试targetContainer.decode(NestedModel.self,forKey:Key(stringValue:lastKey)! )
}
然后您可以像这样使用它:
let deepNestedModel = try DecodingHelper.decode(modelType:Amazon.self,fromKey: nest1 .nest2.nest3)
来自此json:
{
apple:{...},
amazon:{
amzncode: SPOT,
music_quality:好,
stanley:苹果缺席
},
nest1:{
nest2:{
amzncode:嵌套作品,
music_quality:很棒,
stanley:哦,是,
nest3:{
amzncode:嵌套再次正常!,
music_quality:贪婪at,
stanley:哦,是
}
}
}
}
完整代码:
I have the following code to extract a JSON contained within a coding key:
let value = try! decoder.decode([String:Applmusic].self, from: $0["applmusic"])
This successfully handles the following JSONs:
{
"applmusic":{
"code":"AAPL",
"quality":"good",
"line":"She told me don't worry",
}
However, fails to extract a JSON with the coding key of applmusic
from the following one:
{
"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"
}
}
The data models for applmusic
,spotify
and amazon
are different. However, I need only to extract applmusic
and omit other coding keys.
My Swift
data model is the following:
public struct Applmusic: Codable {
public let code: String
public let quality: String
public let line: String
}
The API responds with the full JSON and I cannot ask it to give me only the needed fields.
How to decode only the specific part of the json? It seems, that Decodable
requires me to deserialize the whole json first, so I have to know the full data model for it.
Obviously, one of the solutions would be to create a separate Response
model just to contain the applmusic
parameter, but it looks like a 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
}
Could you propose a better way to deal with such JSON structures?
A little bit more insight
I use it the following technique in the generic extension that automatically decodes the API responses for me. Therefore, I'd prefer to generalize a way for handling such cases, without the need to create a Root
structure. What if the key I need is 3 layers deep in the JSON structure?
Here is the extension that does the decoding for me:
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: $0)[key] else {
throw RestClientError.valueNotFound(codingKey: key)
}
return value
}
return try decoder.decode(Response.self, from: $0)
}
}
}
The API is defined like this:
extension API {
static func getMusic() -> Endpoint<[Applmusic]> {
return Endpoint(method: .get,
path: "/api/music",
codingKey: "applmusic")
}
}
Updated: I made an extension of JSONDecoder
out of this answer, you can check it here: https://github.com/aunnnn/NestedDecodable, it allows you to decode a nested model of any depth with a key path.
You can use it like this:
let post = try decoder.decode(Post.self, from: data, keyPath: "nested.post")
You can make a Decodable
wrapper (e.g., ModelResponse
here), and put all the logic to extract nested model with a key inside that:
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
}
}
You can pass your desired key through userInfo
of JSONDecoder
("my_model_key"
). It is then converted to our dynamic Key
inside ModelResponse
to actually extract the model.
Then you can use it like this:
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)
Full code:https://gist.github.com/aunnnn/2d6bb20b9dfab41189a2411247d04904
Bonus: Deeply nested key
After playing around more, I found you can easily decode a key of arbitrary depth with this modified 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)!)
}
Then you can use it like this:
let deeplyNestedModel = try DecodingHelper.decode(modelType: Amazon.self, fromKey: "nest1.nest2.nest3")
From this 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"
}
}
}
}
Full code: https://gist.github.com/aunnnn/9a6b4608ae49fe1594dbcabd9e607834
这篇关于Swift:可编码-提取单个编码密钥的文章就介绍到这了,希望我们推荐的答案对大家有所帮助,也希望大家多多支持!