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