kanaloa
kanaloa

Reputation: 41

How to parse JSON from Yahoo Finance in Swift for MacOS

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

Answers (3)

kanaloa
kanaloa

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

vadian
vadian

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

Jon Shier
Jon Shier

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

Related Questions