0
votes

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

3
Be aware that all numeric types are wrong. Only numbers in double quotes are String, floating point values are Double, the other are Int, true and false (not in double quotes) are Bool. And Codable is much better than SwiftyJSON. Declare everything non-optional, read the comprehensive errors you get and fix the issues.vadian
Thanks! Good catch on the stuct properties, I'll change those. Once that's done, how do I decode the JSON object? I've tried using JSONDecoder(), but that seems to want a Data object, not a JSON object.kanaloa
Replace .responseJSON with .responseDatavadian
Thanks, I did that, and the error I get is: The data couldn’t be read because it is missing.kanaloa
Well, print(error) in the catch block rather than anything else. It tells you what's wrong. Hint: The root object (with the quoteResponse key) is missing.vadian

3 Answers

3
votes

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.

1
votes

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.
}
0
votes

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