Related
I'm having trouble getting a certain value when I make an API call. The structs are set up like so:
struct Status: Decodable {
let status: String
let results: [Results]
}
struct Results: Decodable {
let title: String
let abstract: String
let url: String
let multimedia: [Multimedia]
}
struct Multimedia: Decodable {
let imageUrl: String
enum CodingKeys: String, CodingKey {
case imageUrl = "url"
}
}
And I'm trying to get the imageUrl member of the Multimedia struct. When I make the data request, I populate an array var storyData = [Results]() with the object.results. I make the data request in my viewDidLoad:
fetchData(url: jsonURL) { (result: FetchResult<Status>) -> (Void) in
switch result {
case .success(let object): self.storyData = object.results
print("\n\nNumber of stories: \(self.storyData.count)\n\nStories: \n\n \(self.storyData)")
case .failure(let error):
print("Error decoding JSON: \n\n \(error)")
}
DispatchQueue.main.async {
self.tableView.reloadData()
}
}
Now, I'm trying to get the imageUrl so I can display the image in a table view, and I thought I'd get it in my cellForRowAt method using something like
let newsStories = storyData[indexPath.row]
cell.storyImageView.cacheImage(urlString: newsStories.multimedia.imageUrl)
However I can't do this, I can access newsStories.multimedia but no further, as if multimedia doesn't have any properties.
Is there a different way that I can get the imageUrl?
[Multimedia] is an array, you have to access it through an index. Then you can access it with imageUrl.
For Example:
cell.storyImageView.cacheImage(urlString: newsStories.multimedia[0].imageUrl)
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 applmusicparameter, 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
You don't really need the nested struct Applmusic inside Response. This will do the job:
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")
}
Edit: Addressing your latest comment
If the JSON response would change in a way that the applmusic tag is nested, you would only need to properly change your Response type. Example:
New JSON (note that applmusic is now nested in a new responseData tag):
{
"responseData":{
"applmusic":{
"code":"AAPL",
"quality":"good",
"line":"She told me don't worry"
},
"I don't want this":"potatoe",
}
}
The only change needed would be in 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)
}
}
The previous changes wouldn't break up any existing code, we are only fine-tuning the private implementation of how the Response parses the JSON data to correctly fetch an Applmusic object. All calls such as JSONDecoder().decode(Response.self, from: data).applmusic would remain the same.
Tip
Finally, if you want to hide the Response wrapper logic altogether, you may have one public/exposed method which will do all the work; such as:
// (fine-tune this method to your needs)
func decodeAppleMusic(data: Data) throws -> Applmusic {
return try JSONDecoder().decode(Response.self, from: data).applmusic
}
Hiding the fact that Response even exists (make it private/inaccessible), will allow you to have all the code through your app only have to call decodeAppleMusic(data:). For example:
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")
}
Recommended read:
Encoding and Decoding Custom Types
https://developer.apple.com/documentation/foundation/archives_and_serialization/encoding_and_decoding_custom_types
Interesting question. I know that it was 2 weeks ago but I was wondering
how it can be solved using library KeyedCodable I created. Here is my proposition with generic:
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)
}
}
helper extension:
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
}
}
use:
let decoder = JSONDecoder()
decoder.userInfo.keyPath = "applmusic"
let response = try? decoder.decode(Response<Applmusic>.self, from: jsonData)
Please notice that keyPath may be nested more deeply I mean it may be eg. "responseData.services.applemusic".
In addition Response is a Codable so you can encode it without any additional work.
I've got some JSON messages coming in over a websocket connection.
// sample message
{
type: "person",
data: {
name: "john"
}
}
// some other message
{
type: "location",
data: {
x: 101,
y: 56
}
}
How can I convert those messages into proper structs using Swift 4 and the Codable protocol?
In Go I can do something like: "Hey at the moment I only care about the type field and I'm not interested in the rest (the data part)." It would look like this
type Message struct {
Type string `json:"type"`
Data json.RawMessage `json:"data"`
}
As you can see Data is of type json.RawMessage which can be parsed later on. Here is a full example https://golang.org/pkg/encoding/json/#example_RawMessage_unmarshal.
Can I do something similar in Swift? Like (haven't tried it yet)
struct Message: Codable {
var type: String
var data: [String: Any]
}
Then switch on the type to convert the dictionary into proper structs. Would that work?
I wouldn't rely upon a Dictionary. I'd use custom types.
For example, let's assume that:
you know which object you're going to get back (because of the nature of the request); and
the two types of response truly return identical structures except the contents of the data.
In that case, you might use a very simple generic pattern:
struct Person: Decodable {
let name: String
}
struct Location: Decodable {
let x: Int
let y: Int
}
struct ServerResponse<T: Decodable>: Decodable {
let type: String
let data: T
}
And then, when you want to parse a response with a Person, it would be:
let data = json.data(using: .utf8)!
do {
let responseObject = try JSONDecoder().decode(ServerResponse<Person>.self, from: data)
let person = responseObject.data
print(person)
} catch let parseError {
print(parseError)
}
Or to parse a Location:
do {
let responseObject = try JSONDecoder().decode(ServerResponse<Location>.self, from: data)
let location = responseObject.data
print(location)
} catch let parseError {
print(parseError)
}
There are more complicated patterns one could entertain (e.g. dynamic parsing of the data type based upon the type value it encountered), but I wouldn't be inclined to pursue such patterns unless necessary. This is a nice, simple approach that accomplishes typical pattern where you know the associated response type for a particular request.
If you wanted you could validate the type value with what was parsed from the data value. Consider:
enum PayloadType: String, Decodable {
case person = "person"
case location = "location"
}
protocol Payload: Decodable {
static var payloadType: PayloadType { get }
}
struct Person: Payload {
let name: String
static let payloadType = PayloadType.person
}
struct Location: Payload {
let x: Int
let y: Int
static let payloadType = PayloadType.location
}
struct ServerResponse<T: Payload>: Decodable {
let type: PayloadType
let data: T
}
Then, your parse function could not only parse the right data structure, but confirm the type value, e.g.:
enum ParseError: Error {
case wrongPayloadType
}
func parse<T: Payload>(_ data: Data) throws -> T {
let responseObject = try JSONDecoder().decode(ServerResponse<T>.self, from: data)
guard responseObject.type == T.payloadType else {
throw ParseError.wrongPayloadType
}
return responseObject.data
}
And then you could call it like so:
do {
let location: Location = try parse(data)
print(location)
} catch let parseError {
print(parseError)
}
That not only returns the Location object, but also validates the value for type in the server response. I'm not sure it's worth the effort, but in case you wanted to do so, that's an approach.
If you really don't know the type when processing the JSON, then you just need to write an init(coder:) that first parses the type, and then parses the data depending upon the value that type contained:
enum PayloadType: String, Decodable {
case person = "person"
case location = "location"
}
protocol Payload: Decodable {
static var payloadType: PayloadType { get }
}
struct Person: Payload {
let name: String
static let payloadType = PayloadType.person
}
struct Location: Payload {
let x: Int
let y: Int
static let payloadType = PayloadType.location
}
struct ServerResponse: Decodable {
let type: PayloadType
let data: Payload
init(from decoder: Decoder) throws {
let values = try decoder.container(keyedBy: CodingKeys.self)
type = try values.decode(PayloadType.self, forKey: .type)
switch type {
case .person:
data = try values.decode(Person.self, forKey: .data)
case .location:
data = try values.decode(Location.self, forKey: .data)
}
}
enum CodingKeys: String, CodingKey {
case type, data
}
}
And then you can do things like:
do {
let responseObject = try JSONDecoder().decode(ServerResponse.self, from: data)
let payload = responseObject.data
if payload is Location {
print("location:", payload)
} else if payload is Person {
print("person:", payload)
}
} catch let parseError {
print(parseError)
}
New on here and to Swift so please go easy on me..
Am a bit stuck when trying to parse JSON which contains nested dictionaries. I imagine its something wrong with the data strutures I have created and I have tryed everthing to rectify but still getting the same issue.
This is the JSON api I am trying to work with:
https://api.coindesk.com/v1/bpi/currentprice.json
These are the data structures I have created to model this:
struct base: Decodable {
let disclaimer: String
let bpi: [Bpi]
}
struct Bpi: Decodable {
let USD: [USD]
}
struct USD: Decodable {
let rate_float: Float
}
And here is my code in the VC :
override func viewDidLoad() {
super.viewDidLoad()
let jsonURLString = "https://api.coindesk.com/v1/bpi/currentprice.json"
guard let url = URL(string: jsonURLString) else {return}
URLSession.shared.dataTask(with: url) { (data, response, err) in
guard let data = data else {return}
do {
let bitcoinData = try JSONDecoder().decode(base.self, from: data)
print(bitcoinData.bpi)
} catch {
print("error")
}
} .resume() // Fires off the session
}
I can grab the data from the disclaimer string or the other strings in the root dictionary but that is it. I cannot parse anything further with the nested dictonaries - it just throws back the catch error.
Here is the JSON:
{
"time": {
"updated": "Nov 2, 2017 06:08:00 UTC",
"updatedISO": "2017-11-02T06:08:00+00:00",
"updateduk": "Nov 2, 2017 at 06:08 GMT"
},
"disclaimer": "This data was produced from the CoinDesk Bitcoin Price Index (USD). Non-USD currency data converted using hourly conversion rate from openexchangerates.org",
"chartName": "Bitcoin",
"bpi": {
"USD": {
"code": "USD",
"symbol": "$",
"rate": "6,889.4013",
"description": "United States Dollar",
"rate_float": 6889.4013
},
"GBP": {
"code": "GBP",
"symbol": "£",
"rate": "5,184.4053",
"description": "British Pound Sterling",
"rate_float": 5184.4053
},
"EUR": {
"code": "EUR",
"symbol": "€",
"rate": "5,910.4587",
"description": "Euro",
"rate_float": 5910.4587
}
}
}
Is there something I am clearly doing wrong here?
Thanks for the help in advance and sorry if my formatting sucks!
Try following model, with this it works - both bpi and USD are not arrays, just single values:
struct base: Decodable {
let disclaimer: String
let bpi: Bpi
}
struct Bpi: Decodable {
let USD: USD
}
struct USD: Decodable {
let rate_float: Float
}
Dictionaries (Dictionary<K,V>) are implicitly Decodable compliant if both generic types K and V are decodable.
Assuming you create a struct Coin for the currencies
struct Coin: Decodable {
private enum CodingKeys : String, CodingKey {
case code, symbol, rate, description, rateFloat = "rate_float"
}
let code : String
let symbol : String
let rate : String
let description : String
let rateFloat : Float
}
you can easily decode the currency dictionaries as [String:Coin] without any additional code
struct Base: Decodable {
private enum CodingKeys : String, CodingKey {
case disclaimer, coins = "bpi"
}
let disclaimer: String
let coins: [String:Coin]
}
And use it
let bitcoinData = try JSONDecoder().decode(Base.self, from: data)
print(bitcoinData.coins)
Alternatively if you want the currencies as an array of Coin you can write a custom initializer and map the dictionary values to an array.
This example decodes also the updatedISO value in the time dictionary
struct Base: Decodable {
struct Time : Decodable {
private enum CodingKeys : String, CodingKey {
case updated = "updatedISO"
}
let updated : Date
}
private enum CodingKeys : String, CodingKey {
case disclaimer, bpi, time
}
let disclaimer: String
let coins: [Coin]
let updated : Date
init(from decoder: Decoder) throws {
let container = try decoder.container(keyedBy: CodingKeys.self)
disclaimer = try container.decode(String.self, forKey: .disclaimer)
let bpi = try container.decode([String:Coin].self, forKey: .bpi)
coins = Array(bpi.values.sorted(by: {$0.code < $1.code}))
let time = try container.decode(Time.self, forKey: .time)
updated = time.updated
}
}
And use this example
let decoder = JSONDecoder()
decoder.dateDecodingStrategy = .iso8601
let bitcoinData = try decoder.decode(Base.self, from: data)
print(bitcoinData.coins)
You declared your bpi & use properties as arrays but they were dictionaries (nested json objects). If you have sample JSON you can try this converter next time: https://danieltmbr.github.io/JsonCodeGenerator/
It generates the following output:
struct Root: Codable {
let time: Time
let disclaimer: String
let chartName: String
let bpi: Bpi
}
struct Time: Codable {
let updated: String
let updatedISO: String
let updateduk: String
}
struct Bpi: Codable {
let USD: USD
let GBP: USD
let EUR: USD
}
struct USD: Codable {
let code: String
let symbol: String
let rate: String
let description: String
let rateFloat: Double
private enum CodingKeys: String, CodingKey {
case code
case symbol
case rate
case description
case rateFloat = "rate_float"
}
}
I know this has been covered in other questions, but I've followed them and I'm still stumped. Here is my JSON structure:
{
"FindBoatResult": {
"num_boats": 10,
"boat": [
{
"num_segments": 1,
"segments": [
{
"ident": "String",
"origin" : {
"code" : "String"
},
},
]
}
etc...but thats as deep as the structure goes. there are multiple returns of "segments" in each JSON response. In Swift I have this code.
struct Result : Decodable {
let FindBoatResult : FindBoatResult
}
struct FindBoatResult : Decodable {
let boats : Boats
let num_boats : Int
}
struct Boats : Decodable {
let segments : [Segments]
}
struct Segments : Decodable {
let ident : String?
let origin : Origin
}
struct Origin : Decodable {
let code : String
}
func getBoats() {
let urlString = "http://myApi"
guard let url = URL(string: urlString) else { return }
URLSession.shared.dataTask(with: url) { (data, response, err) in
guard let data = data else {return}
let dataAsString = String(data: data, encoding: .utf8)
//print(dataAsString)
do {
let boats = try
JSONDecoder().decode(FindBoatResult.self, from: data)
print(boats)
} catch {
print(err)
}
}.resume()
}
This fails and throws err but err prints as nil..so I can't tell what I'm missing. dataAsString prints out the JSON as expected, so I know "data" is good.
I detected a couple of minor issues. Try replacing this:
struct FindBoatResult: Decodable {
let boats: Boats
let num_boats: Int
}
struct Boats: Decodable {
let segments: [Segments]
}
with:
struct FindBoatResult: Decodable {
let boat: [Boat]
let num_boats: Int
}
struct Boat: Decodable {
let segments: [Segments]
}
Finally, decode using the Result type (not FindBoatResult):
JSONDecoder().decode(Result.self, from: data)
Expanding on Paulo's answer, I might further suggest that if you're stuck with JSON that has keys that don't conform to Swift conventions for property names, that you use the CodingKeys pattern to translate JSON keys to better Swift property names, e.g.:
struct BoatResult: Decodable { // I'd simplify this name
let boatCollection: BoatCollection
enum CodingKeys: String, CodingKey {
case boatCollection = "FindBoatResult"
}
}
struct BoatCollection: Decodable { // I'd simplify this, too, removing "Find" from the name; verbs are for methods, not properties
let boats: [Boat]
let numberOfBoats: Int
enum CodingKeys: String, CodingKey {
case boats = "boat" // "boat" isn't great property name for an array of boats, so let's map the poor JSON key to better Swift name here
case numberOfBoats = "num_boats" // likewise, let's map the "_" name with better camelCase property name
}
}
struct Boat: Decodable { // This entity represents a single boat, so let's use "Boat", not "Boats"
let segments: [Segment]
}
struct Segment: Decodable { // This entity represents a single segment, so let's use "Segment", not "Segments"
let identifier: String
let origin: Origin
enum CodingKeys: String, CodingKey {
case identifier = "ident" // `ident` isn't a common name for identifier, so let's use something more logical
case origin
}
}
struct Origin: Decodable {
let code: String
}
So, for example, use a plurals (e.g. boats) when you're representing an array of objects, and use CodingKeys to map the misleading boat JSON key to this better named boats array reference. Or when you have a key like num_boats, don't feel like you have to use that bad name in your Swift property and use something better like numberOfBoats (or count or whatever), and lose the _ syntax which is very unSwifty.
Clearly, if you're in control of the design of the JSON, you can just fix some of these poorly chosen key names there, but even where you decide you want your web service to use the _ syntax, go ahead and use CodingKeys to make sure your Swift objects honor the camelCase convention.