コーディングキーに含まれるJSONを抽出するための次のコードがあります。
let value = try! decoder.decode([String:Applmusic].self, from: $0["applmusic"])
これにより、次のJSONが正常に処理されます。
{
"applmusic":{
"code":"AAPL",
"quality":"good",
"line":"She told me don't worry",
}
ただし、コーディングキーが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全体を逆シリアル化する必要があるようです。そのため、その完全なデータモデルを知る必要があります。
明らかに、解決策の1つは、Response
パラメータを含めるためだけに別のapplmusic
モデルを作成することですが、ハックのように見えます。
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: $0)[key] else {
throw RestClientError.valueNotFound(codingKey: key)
}
return value
}
return try decoder.decode(Response.self, from: $0)
}
}
}
APIは次のように定義されています。
extension API {
static func getMusic() -> Endpoint<[Applmusic]> {
return Endpoint(method: .get,
path: "/api/music",
codingKey: "applmusic")
}
}
更新:この回答から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
}
}
userInfo
のJSONDecoder
を介して目的のキーを渡すことができます("my_model_key"
)。次に、Key
内の動的なModelResponse
に変換され、実際にモデルが抽出されます。
次に、次のように使用できます。
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
Applmusic
内にネストされた構造体Response
は実際には必要ありません。これは仕事をします:
_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")
}
_
編集:最新のコメントに対処する
applmusic
タグがネストされるようにJSON応答が変更される場合は、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
ラッパーロジックを完全に非表示にする場合は、すべての作業を実行する1つの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
が存在するという事実を隠す(プライベート/アクセス不能にする)と、アプリからすべてのコードを取得できるようになりますonlydecodeAppleMusic(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")
}
_
推奨読書:
カスタムタイプのエンコードとデコード
興味深い質問です。 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であるため、追加の作業なしでエンコードできます。