Reputation: 41
I've been struggling with this one! I've got Alamofire and SwiftyJSON. I use Alamofire to get a JSON result from Yahoo Finance like this:
public func getYahooQuote(symbol: String) {
let stockURL = "https://query1.finance.yahoo.com/v7/finance/quote?symbols=" + symbol
let request = AF.request(stockURL, parameters: ["quoteResponse": "result"])
request.responseData { (response) in
guard let data = response.value else {return}
do {
let json = try JSON(data: data)
print(json)
let decoder = JSONDecoder()
let stock = try decoder.decode(QuoteParent.self, from: data)
print(stock)
} catch {
print(error)
}
}
}
So that request takes a string variable symbol which is passed into the function. The result I get is a JSON object that prints this: '
{
"quoteResponse" : {
"result" : [
{
"fiftyTwoWeekLow" : 164.93000000000001,
"regularMarketVolume" : 33445281,
"messageBoardId" : "finmb_8108558",
"symbol" : "QQQ",
"currency" : "USD",
"regularMarketPreviousClose" : 258.00999999999999,
"fiftyDayAverage" : 250.32285999999999,
"exchange" : "NMS",
"quoteType" : "ETF",
"regularMarketDayLow" : 251.31999999999999,
"averageDailyVolume10Day" : 46768962,
"fiftyTwoWeekHighChange" : -15.310013,
"priceHint" : 2,
"twoHundredDayAverageChange" : 31.669998,
"exchangeTimezoneName" : "America\/New_York",
"bookValue" : 188.77500000000001,
"firstTradeDateMilliseconds" : 921076200000,
"averageDailyVolume3Month" : 42292663,
"tradeable" : false,
"bidSize" : 8,
"sourceInterval" : 15,
"regularMarketChange" : -3.530014,
"triggerable" : true,
"longName" : "Invesco QQQ Trust",
"market" : "us_market",
"exchangeTimezoneShortName" : "EDT",
"regularMarketDayHigh" : 256.93000000000001,
"marketCap" : 100036083712,
"gmtOffSetMilliseconds" : -14400000,
"fiftyTwoWeekHighChangePercent" : -0.056747886999999997,
"askSize" : 10,
"language" : "en-US",
"marketState" : "REGULAR",
"fiftyTwoWeekRange" : "164.93 - 269.79",
"twoHundredDayAverage" : 222.81,
"trailingAnnualDividendRate" : 1.54,
"quoteSourceName" : "Delayed Quote",
"trailingThreeMonthReturns" : 30.27,
"fiftyDayAverageChange" : 4.1571350000000002,
"shortName" : "Invesco QQQ Trust, Series 1",
"fiftyDayAverageChangePercent" : 0.016607093,
"region" : "US",
"regularMarketTime" : 1595609084,
"priceToBook" : 1.3480599,
"regularMarketOpen" : 254.12,
"fiftyTwoWeekLowChange" : 89.549999999999997,
"regularMarketDayRange" : "251.32 - 256.93",
"trailingAnnualDividendYield" : 0.0059687606999999998,
"fullExchangeName" : "NasdaqGS",
"regularMarketChangePercent" : -1.3681694,
"trailingPE" : 65.335044999999994,
"fiftyTwoWeekHigh" : 269.79000000000002,
"bid" : 254.56,
"epsTrailingTwelveMonths" : 3.895,
"trailingThreeMonthNavReturns" : 30.210000000000001,
"fiftyTwoWeekLowChangePercent" : 0.54295766000000001,
"twoHundredDayAverageChangePercent" : 0.14213903,
"ask" : 254.61000000000001,
"esgPopulated" : false,
"regularMarketPrice" : 254.47999999999999,
"sharesOutstanding" : 393100000,
"financialCurrency" : "USD",
"exchangeDataDelayedBy" : 0,
"ytdReturn" : 16.809999999999999
}
],
"error" : null
}
}
I've got Codable structs like this:
struct QuoteParent: Codable {
var quoteResponse: QuoteResponse
}
struct QuoteResponse: Codable {
var error: QuoteError?
var result: Stock?
}
struct QuoteError: Codable {
var lang: String?
var description: String?
var message: String?
var code: Int
}
struct Stock: Codable {
var ask : Decimal
var askSize : Int
var averageDailyVolume10Day : Int
var averageDailyVolume3Month : Int
var bid : Double
var bidSize : Int
var bookValue : Decimal
var currency : String
var epsTrailingTwelveMonths : Decimal
var esgPopulated : Bool
var exchange : String
var exchangeDataDelayedBy : Int
var exchangeTimezoneName : String
var exchangeTimezoneShortName : String
var fiftyDayAverage : Decimal
var fiftyDayAverageChange : Decimal
var fiftyDayAverageChangePercent : Decimal
var fiftyTwoWeekHigh : Decimal
var fiftyTwoWeekHighChange : Decimal
var fiftyTwoWeekHighChangePercent : Decimal
var fiftyTwoWeekLow : Decimal
var fiftyTwoWeekLowChange : Decimal
var fiftyTwoWeekLowChangePercent : Decimal
var fiftyTwoWeekRange : String?
var financialCurrency : String
var firstTradeDateMilliseconds : Int
var fullExchangeName : String
var gmtOffSetMilliseconds : Int
var language : String
var longName : String
var market : String
var marketCap : Int
var marketState : String
var messageBoardId : String
var priceHint : Int
var priceToBook : Decimal
var quoteSourceName : String
var quoteType : String
var region : String
var regularMarketChange : Int
var regularMarketChangePercent : Decimal
var regularMarketDayHigh : Decimal
var regularMarketDayLow : Decimal
var regularMarketDayRange : String
var regularMarketOpen : Double
var regularMarketPreviousClose : Decimal
var regularMarketPrice : Decimal
var regularMarketTime : Int
var regularMarketVolume : Int
var sharesOutstanding : Int
var shortName : String
var sourceInterval : Int
var symbol : String
var tradeable : Bool
var trailingAnnualDividendRate : Double
var trailingAnnualDividendYield : Decimal
var trailingPE : Decimal
var trailingThreeMonthNavReturns : Decimal
var trailingThreeMonthReturns : Decimal
var triggerable : Bool
var twoHundredDayAverage : Double
var twoHundredDayAverageChange : Decimal
var twoHundredDayAverageChangePercent : Decimal
var ytdReturn : Decimal
}
I've tried to decode that using JSONDecoder, but that seems to need a Data object, while the object I get is JSON.
I use this line to narrow the JSON object to just the value of result like this:
let json2 = json["quoteResponse"]["result"]
Now that's still just a JSON object, which does contain all the data I want, but I have not been able to figure out how to parse that JSON object to the Struct class I have. Any wisdom here would be so appreciated!
I did try this to get the JSON:
request.responseData { (response) in
instead of
request.responseJSON { (response) in
And attempted to decode it with:
let decoder = JSONDecoder()
let stock = try decoder.decode(Stock.self, from: data)
But now the error I get prints like this:
typeMismatch(Swift.Dictionary<Swift.String, Any>, Swift.DecodingError.Context(codingPath: [CodingKeys(stringValue: "quoteResponse", intValue: nil), CodingKeys(stringValue: "result", intValue: nil)], debugDescription: "Expected to decode Dictionary<String, Any> but found an array instead.", underlyingError: nil))
Upvotes: 0
Views: 1864
Reputation: 41
Really want to thank everyone for your help, especially vadian. Here's the final working code based on vadian's suggestion.
First, here's the new get Yahoo Finance quote function:
func getYahooQuote(symbol: String, completion: @escaping (QuoteParent) -> Void) {
var quoteParent = QuoteParent()
let stockURL = "https://query1.finance.yahoo.com/v7/finance/quote?symbols=" + symbol
let request = AF.request(stockURL, parameters: ["quoteResponse": "result"])
request.responseData { (response) in
guard let data = response.value else {return}
do {
let json = try JSON(data)
print(json)
let decoder = JSONDecoder()
quoteParent = try decoder.decode(QuoteParent.self, from: data)
completion(quoteParent)
} catch {
print(error)
}
}
}
Here's the updated Structs to hold the data:
struct QuoteParent: Codable {
var quoteResponse: QuoteResponse
init() {
quoteResponse = QuoteResponse()
}
}
struct QuoteResponse: Codable {
var error: QuoteError?
var result: [Stock]?
init() {
error = nil
result = []
}
}
struct QuoteError: Codable {
var lang: String?
var description: String?
var message: String?
var code: Int?
init() {
lang = ""
description = ""
message = ""
code = 0
}
}
struct Stock: Codable {
var ask : Decimal?
var askSize : Int?
var averageDailyVolume10Day : Int?
var averageDailyVolume3Month : Int?
var bid : Double?
var bidSize : Int?
var bookValue : Decimal?
var currency : String?
var epsTrailingTwelveMonths : Decimal?
var esgPopulated : Bool?
var exchange : String?
var exchangeDataDelayedBy : Int?
var exchangeTimezoneName : String?
var exchangeTimezoneShortName : String?
var fiftyDayAverage : Decimal
var fiftyDayAverageChange : Decimal?
var fiftyDayAverageChangePercent : Decimal?
var fiftyTwoWeekHigh : Decimal?
var fiftyTwoWeekHighChange : Decimal?
var fiftyTwoWeekHighChangePercent : Decimal?
var fiftyTwoWeekLow : Decimal?
var fiftyTwoWeekLowChange : Decimal?
var fiftyTwoWeekLowChangePercent : Decimal?
var fiftyTwoWeekRange : String?
var financialCurrency : String?
var firstTradeDateMilliseconds : Int?
var fullExchangeName : String?
var gmtOffSetMilliseconds : Int?
var language : String?
var longName : String?
var market : String?
var marketCap : Int?
var marketState : String?
var messageBoardId : String?
var priceHint : Int?
var priceToBook : Decimal?
var quoteSourceName : String?
var quoteType : String?
var region : String?
var regularMarketChange : Decimal?
var regularMarketChangePercent : Decimal?
var regularMarketDayHigh : Decimal?
var regularMarketDayLow : Decimal?
var regularMarketDayRange : String?
var regularMarketOpen : Double?
var regularMarketPreviousClose : Decimal?
var regularMarketPrice : Decimal?
var regularMarketTime : Int?
var regularMarketVolume : Int?
var sharesOutstanding : Int?
var shortName : String?
var sourceInterval : Int?
var symbol : String?
var tradeable : Bool?
var trailingAnnualDividendRate : Double?
var trailingAnnualDividendYield : Decimal?
var trailingPE : Decimal?
var trailingThreeMonthNavReturns : Decimal?
var trailingThreeMonthReturns : Decimal?
var triggerable : Bool?
var twoHundredDayAverage : Double?
var twoHundredDayAverageChange : Decimal?
var twoHundredDayAverageChangePercent : Decimal?
var ytdReturn : Decimal?
}
I decided to make the properties optional since I found that the JSON results don't always have the same fields, such as ETFs vs Mutual Funds.
Here's how I implement the function from other view controllers...
@IBAction func symbolAction(_ sender: NSTextField) {
let investment = investmentsArrayController.selectedObjects[0] as! InvestmentMO
if investment.symbol?.count == 5 && investment.symbol?.suffix(2) == "XX" {
investment.investmentType = TypeOfInvestment.CASH
investment.investmentTypeString = investment.investmentType.displayName
} else if investment.symbol?.count == 5 && investment.symbol?.suffix(1) == "X" {
investment.investmentTypeString = TypeOfInvestment.MF.displayName
}
app.myViewController.getYahooQuote(symbol: investment.symbol ?? "", completion: {(quoteParent) -> Void in
let stock = quoteParent.quoteResponse.result?[0]
investment.investmentName = stock?.longName?.uppercased() ?? ""
investment.price = NSDecimalNumber(decimal: stock?.regularMarketPrice ?? stock?.ask ?? 0)
investment.priceChange = NSDecimalNumber(decimal: stock?.regularMarketChange ?? 0)
investment.priceChangePerc = NSDecimalNumber(decimal: stock?.regularMarketChangePercent ?? 0).dividing(by: 100)
investment.prevPrice = NSDecimalNumber(decimal: (stock?.regularMarketPreviousClose ?? investment.price?.decimalValue) ?? 0)
})
}
Upvotes: 0
Reputation: 285150
The error is very descriptive: The value of the key result
in the object for key quoteResponse
[CodingKeys(stringValue: "quoteResponse", intValue: nil), CodingKeys(stringValue: "result", intValue: nil)]
is not a dictionary, it is an array
Expected to decode Dictionary<String, Any> but found an array instead
So change
let result: [Stock]
You can declare all other properties as constants (let
), too.
Upvotes: 3
Reputation: 12770
You can use tools like quicktype.io to generate Codable
types from JSON, so I suggest you use that to get started and go from there.
I also suggest you use Alamofire's responseDecodable
to parse your responses once you have a Decodable
type.
AF.request(...).responseDecodable(of: YourType.self) { response in
// Handle response.
}
Upvotes: 1