Receiving Websocket data in Swift - json

I'm carrying this on from this question, since the focus has changed.
I am trying to send string data from a vapor server over a websocket. The client side is where the main question is. This code successfully receives the string, which is expected to be JSON (but not absolutely guaranteed -- out of scope).
switch message {
case .data(let data):
print("data: \(data)")
case .string(let str):
// let data = str.message(using: .utf8)
let jsonData = Data(str.utf8)
print("string: \(jsonData)")
do {
struct Person : Codable {
var name: String
}
let decoder = JSONDecoder()
let people = try decoder.decode([Person].self, from: jsonData)
print("result: \(people)")
} catch {
print(error.localizedDescription)
}
}
After some very helpful guidance, sending a string such as "{\"name\": \"Bobberoo\"}" will print out
string: 20 bytes
The data couldn’t be read because it isn’t in the correct format.
If I wrap it in braces "[{\"name\": \"Bobberoo\"}]" produces the more helpful but still mystifing (to me) output:
result: [wb2_socket_client.WebSocketController.(unknown context at $101a35028).(unknown context at $101a350c0).(unknown context at $101a35158).Person(name: "Bobberoo")]
Clearly, the decoding is happening, but it's wrapped in these contexts. What are they? I can see that the first is the instance of the WebSocketController. How do I access this data.
And as a non-inflammatory aside: managing JSON is a trivial operation in any number of contexts. Python/Flask, Node, Ruby/Rails and on and on; I've used all these and implementing this kind of interaction is trivial. In Swift, it's a horrible, underdocumented nightmare. At least, that's my experience. Why? I know the language is type safe, but this is ridiculous.

error.localizedDescription won't give you an error message that is useful message for debugging. On the other hand, if you print error directly:
print(error)
You'd get something along the lines of "expected to decode array but found dictionary instead", which is exactly what is happening in the case of
{
"name": "Bobberoo"
}
You are decoding a [Person].self, i.e. an array of Person, but your JSON root is not a JSON array. The above JSON can be decoded if you did:
let people = try decoder.decode(Person.self, from: jsonData)
Clearly, the decoding is happening, but it's wrapped in these contexts. What are they?
This is the default string representation of a type. Your Person struct does not conform to CustomStringConvertible or CustomDebugStringConvertible or TextOutputStreamable, so "an unspecified result is supplied automatically by the Swift standard library" (the link points to String.init(reflecting:), which presumably gets called somewhere along the way when you print the array of Person) and used as the string representation.
From what I can see, its current implementation is the fully qualified name of the struct - starting with the module, then the top-level class, then each enclosing scope, ending with the struct name, followed by the struct's members in brackets. It turns out that the enclosing scopes has no "names", and so are just called (unknown context at xxxxx). This is all very much implementation details, and things that you shouldn't care about.
What you should do, is provide an implementation of CustomStringConvertible:
struct Person: CustomStringConvertible {
...
var description: String { "name: \(name)" }
}
Now printing people gives:
[name: Bobberoo]
I can see that the first is the instance of the WebSocketController.
No. The WebSocketController is part of the fully qualified name of your Person struct. There is exactly one instance in your decoded array, and it's an instance of Person, as you would expect!
How do I access this data?
To access its name:
if let firstPerson = people.first {
let firstPersonsName = firstPerson.name
}

Related

Swift - Parsing JSON with Combine, URLSession DataTaskPublisher - Strange formatting

I'm looking for some guidance here on an issue I'm encountering. I am polling an API for stock data and instead of getting back an array or formatted JSON I get chunks of it. Each block of JSON seems to be properly formatted, but the JSON decoders are looking for a properly formatted array.
Prior to using combine I had a different working. I had to parse the data to remove 4 trailing characters, then I was left with lines of JSON that I could split by newline and then do a forEach loop on to parse.
This was messy but it works. The issue now is I'm polling streaming data for live quote updating in my UI. I need to have the JSON chunks read into my class, stored as a variable that will update the UI. I am not worried about all of that code once I can figure out how to get the data read properly.
I know my app is receiving the data because I see the stats in the debugger for network. I also test the same API call in a browser and see what data my app is receiving. I just don't know how to break it up in the combine framework.
I have been working with something like this so far:
return URLSession.shared.dataTaskPublisher(for: request)
.map { $0.data }
.handleEvents(receiveSubscription: { print("Receive subscription: \($0)") },
receiveOutput: { print("Receive output: \($0)") },
receiveCompletion: { print("Receive completion: \($0)") },
receiveCancel: { print("Receive cancel") },
receiveRequest: { print("Receive request: \($0)") })
//.decode(type: CryptoDataContainer.self, decoder: JSONDecoder())
.decode(type: Quote.self, decoder: JSONDecoder())
.receive(on: DispatchQueue.main)
.eraseToAnyPublisher()
}
The Quote struct is very basic:
struct Quote: Codable {
public var Symbol: String
public var Last: Double
public var Bid: Double
public var Ask: Double
public var Close: Double
}
JSON data is posted below, and it streams. I get a few chunks of it at a time, but if I can split by newline or by everything {.*} to get each JSON chunk and decode individually it would work great.
I'm really looking for Last price and Symbol out of the JSON. I could even just look for Last and start one individual streaming session for each symbol I need.
I will also need to split these up using a subject to multicast each publisher to multiple subscribers but I wanted to get the basic functionality working before complicating matters.
Update: From another post I found that the problem is the JSON formatting - The API is returning invalid JSON that is not formatted into an array - it is missing the surrounding []. This is why it doesn't parse. Another option may be to simply convert to string, add square brackets around all of the JSON passed down from the server (there is nothing but objects).. and then parse it correctly? I'm not sure how to do this within a combine map but that seems to be the path forward.
Here’s an example of the map I’m trying to use currently, since the data comes in as json chunks I need to parse them each individually.
.map ({
let stringRepresentation = String(data: $0, encoding: .utf8)
let outputJson = stringRepresentation!.split(whereSeparator: \.isNewline).map(String.init)
if outputJson.count > 1 {
print("DEBUG: outputJson.count > 1 code running..")
let formattedOutputJson:String = outputJson.joined(separator: ",")
let finalJson:String = "{\"quotes\":[" + formattedOutputJson + "]}"
let data:Data? = finalJson.data(using: .utf8)
return data!
} else {
print("DEBUG: outputJson.count = 1 (Else) code running..")
let finalJson:String = "{\"quotes\":[" + outputJson[0] + "]}"
let data:Data? = finalJson.data(using: .utf8)
return data!
}
})
Here is a better example of the JSON formatting in chunks I’m talking about:
{"heartbeat":4,"timestamp":"\/Date(1622898090819)\/"}
{"heartbeat":5,"timestamp":"\/Date(1622898095819)\/"}
{"heartbeat":6,"timestamp":"\/Date(1622898100819)\/"}
{"heartbeat":7,"timestamp":"\/Date(1622898105819)\/"}
{"heartbeat":8,"timestamp":"\/Date(1622898110819)\/"}
If I can even capture the heartbeat number as an Int to use I would be well on my way to getting my code working. The issue is I get zero data out of this. I know I’m connected from packet captures and various other tests. The data is getting to my system, and to the Xcode simulated app, I just don’t think I’m getting it to a point where I can print to the console correctly to display it. I have been working on that prior to attempting to update the UI directly.
{"Description":"E-Mini NASDAQ-100 Jun 2021","PreviousClose":13529.25,"PreviousVolume":552987,"AssetType":"FUTURE","CountryCode":"United States","Halted":false,"Bid":13513.75,"BidSize":3,"Ask":13514.25,"AskSize":1,"Currency":"USD","LastSize":1,"LastVenue":"CME","SymbolRoot":"NQ","Exchange":"CME","LastTradingDate":"2021-06-18","DisplayType":3,"FractionalDisplay":false,"MinPrice":1258200,"PointValue":20,"TradeTime":"2021-06-04T02:40:20.0000000+00:00","MaxPrice":1447600,"MinMove":25,"High52Week":14064,"Open":13531.75,"Low52Week":9591.75,"High":13541.75,"Low":13467.75,"Close":13513.75,"Symbol":"NQM21","Volume":23877,"DailyOpenInterest":231736,"ExpirationDate":"2021-06-18","Last":13513.75,"NameExt":"(D)","DataFeed":"TradeStation","IsDelayed":true,"Restrictions":[null],"VWAP":13499.877658013,"NetChange":-15.50,"NetChangePct":-0.1145665872091948925476282900,"High52WeekTimeStamp":"2021-04-29T00:00:00.0000000+00:00","Low52WeekTimeStamp":"2020-06-11T00:00:00.0000000+00:00","PreviousClosePriceDisplay":"13529.25","BidPriceDisplay":"13513.75","AskPriceDisplay":"13514.25","MinPriceDisplay":"1258200.00","MaxPriceDisplay":"1447600.00","High52WeekPriceDisplay":"14064.00","OpenPriceDisplay":"13531.75","Low52WeekPriceDisplay":"9591.75","HighPriceDisplay":"13541.75","LowPriceDisplay":"13467.75","ClosePriceDisplay":"13513.75","LastPriceDisplay":"13513.75","VWAPDisplay":"13499.88","FirstNoticeDate":"","StrikePrice":0,"TickSizeTier":0,"Underlying":"","StrikePriceDisplay":"0.00"}
{"Last":4188.5,"NameExt":"(D)","DataFeed":"TradeStation","IsDelayed":true,"Restrictions":[null],"VWAP":4184.92506004117,"Description":"E-mini S&P 500 Jun 2021","PreviousClose":4191.25,"PreviousVolume":1384271,"AssetType":"FUTURE","CountryCode":"United States","Halted":false,"Bid":4188.25,"BidSize":39,"Ask":4188.5,"AskSize":23,"Currency":"USD","LastSize":1,"LastVenue":"CME","SymbolRoot":"ES","Exchange":"CME","LastTradingDate":"2021-06-18","DisplayType":3,"FractionalDisplay":false,"MinPrice":3898,"PointValue":50,"TradeTime":"2021-06-04T02:40:10.0000000+00:00","MaxPrice":4484,"MinMove":25,"High52Week":4238.25,"Open":4190.75,"Low52Week":2977.8999,"Hi{"Volume":23880,"TradeTime":"2021-06-04T02:40:23.0000000+00:00","VWAP":13499.879455674,"Symbol":"NQM21"}
{"AskSize":18,"Symbol":"ESM21"}
{"Bid":13514.25,"BidSize":1,"AskSize":1,"BidPriceDisplay":"13514.25","Symbol":"NQM21","VWAP":13499.8800582884,"Volume":23881,"TradeTime":"2021-06-04T02:40:25.0000000+00:00"}
{"VWAP":4184.92521199492,"AskSize":19,"Volume":47807,"TradeTime":"2021-06-04T02:40:27.0000000+00:00","Symbol":"ESM21"}
{"Bid":13514,"BidSize":3,"BidPriceDisplay":"13514.00","Symbol":"NQM21","TradeTime":"2021-06-04T02:40:27.0000000+00:00","Ask":13514.75,"VWAP":13499.8806713352,"Close":13514.5,"Last":13514.5,"Volume":23882,"NetChange":-14.75,"NetChangePct":-0.1090230426668144945211301400,"AskPriceDisplay":"13514.75","ClosePriceDisplay":"13514.50","LastPriceDisplay":"13514.50"}
{"AskSize":18,"Symbol":"ESM21"}

looking for most efficient way to display JSON data received via websocket, into a Xcode/Swift dynamic table

Currently I am working on a MacOS financial market trading application that could receive 10-20 websocket messages per second containing JSON data. For now, let's assume that once the data has been displayed in a table and a new message has been received, the previous data is no longer needed. What would be the recommended, and most efficient way to get the data into the table, and to keep updating it as fast as possible?
I have the websocket connection setup and working properly (using SocketRocket. Data is coming in as it should. But I really don't want to continue further without having a better understanding of the most efficient way to present that data into the table. I was reading about DictionaryArrayController, and maybe utilizing a database library, but I think the latter would would only add unnecessary overhead for what I am trying to do.
Below is a sample of the data that I am receiving.
{
"id":"5267",
"pt":"T",
"ls":["1"],
"lp":["11968"],
"t":["1571824228"],
"v":"133758",
"h":"11981",
"l":"11928",
"bp":["0","1","2","3","4","5","6","7","8","9"],
"bs":["14","73","87","66","74","96","98","85","111","104"],
"ap":["1","2","3","4","5","6","7","8","9","10"],
"as":["69","67","62","124","89","105","97","107","113","124"]
}
The only values that I will need to display & update to the table will be coming from keys: "ls" "lp" "v" "h" "l" "bs" "as"
I don't really need a detailed coded response, but I'd never refuse it, either. Mainly just looking for thoughts so I don't have to switch to a different way, later.
I'll also attach an screen capture of an Excel file showing about how the cells will be laid out. screenshot of desired layout
Have you tried using a JSON decoder, built in to Swift?
Create an appropriate using model, using the Decodable protocol. The property names and data types must match the incoming JSON exactly
struct IncomingData: Decodable {
{
let ls: [String],
let lp: [String],
let h: String,
let as: [String]
}
The set up the parse like this:
func parseJSON(_ incomingData: Data) -> IncomingData? {
let decoder = JSONDecoder()
do {
let parsedData = try decoder.decode(IncomingData.self, from: incomingData)
print("as: \(as)"
return parsedData
}
catch {
self.delegate?.didFailWithError(error: error)
return nil
}
}

escaped json string on Swift

In an iPhone app using SWIFT, I'm having to deal with a third-party API that sends a escaped string instead of a JSON object as response.
The response looks like this:
"[
{
\"ID\":3880,
\"Name\":\"Exploration And Production Inc.\",
\"ContractNumber\":\"123-123\",
\"Location\":\"Booker #1\",
\"Volume\":1225.75,
\"OtherFees\":10.0
}
]"
Up until now I have been dealing with this by manipulating the string to remove the unwanted characters until I get a JSON-like string and then parsing that as usual.
Angular has a handy function to deal with this:
angular.fromJson(response.data);
Java has its own way to deal with it. Is an equivalent function in Swift?
If you are parsing it to a dictionary then the simplest solution will be to convert String to Data and use JSONSerialization:
if let data = string.data(using: .utf8) {
do {
let responseArray = try JSONSerialization.jsonObject(with: data, options: []) as? [[String: Any]]
print(responseArray)
}
catch {
print(error)
}
}
But ofcourse it would be better to process it as a Codable model in which case its again just as simple as:
try JSONDecoder().decode([Item].self, from: data)
Provided that you have a valid Decodable model like so:
struct Item: Decodable {
let id: Int
let name: String
//add the other keys you want to use (note its type sensitive)
enum CodingKeys: String, CodingKey {
case id = "ID"
case name = "Name"
}
}
Lastly, avoid stringified jsons because its an easy source of errors.
Malformed strings or small/large deviations in the structure can go easily unnoticed.
Let the backend team know that they should follow a protocol in their API that its consumers can rely on.
By setting the json format, its essentially like a contract that showcases its content and purpose with clarity.
Sending a stringified json is simply lazy and reflects poorly on its designer, imho.

How can I decode a generic JSON response in Swift 4?

I'm building an app with Swift 4 that consumes a JSON-RPC API. The responses all have the same general format:
{
"jsonrpc": "2.0",
"result" : { "data_type" : [ ...a bunch of instances of data_type... ] }
"id": 1
}
Where data_type would be payments, channels, peers, and so on depending on the query.
I have Decodable struct definitions for each of the data types, but I don't know how to handle the main response.
I really don't care about the jsonrpc or id fields, I'm just interested in the contents of result.
I tried:
struct LightningRPCResponse: Decodable {
let id: Int
let result: String
let json_rpc: String
}
But I got the error:
Expected to decode String but found a dictionary instead
So I tried:
struct LightningRPCResponse: Decodable {
let id: Int
let result: Dictionary
let json_rpc: String
}
But I got the error:
Reference to generic type 'Dictionary' requires arguments in <...>
Is what I'm trying to do possible or do I need to create separate response decoders to correspond to every single RPC request?
Or...should I just use string manipulation to lop off the superfluous data?
You could make two structs:
struct generalStruct:Codable {
let jsonrpc:String
let id:Int
let result:[resultsStruct]
}
struct resultsStruct{
//assuming that you have strings in here, cause you didn't specify that. And it's considered as a Dictionary like: "data_tupe":"string_value" or if you have an array also here than just make another struct or just make data_type:[String]
let data_type:String
}
With that structs you can decode now. Example:
let json = try decoder.decode(generalStruct.self, from: response.data!)
//here you can get access to each element of your 'data_type'
for obj in json.result{
for data in obj.data_type {
//you have every element from dict access here if its more objects inside every 'data_type'
}
}

Swift 3 parsing values from a json file

Swift 3 , Xcode8.2.1,
I'm trying to extract specific values from a json file in the project. The name of the file is city.list.json, and the syntax of the json file is as follows:
{"_id":707860,"name":"Hurzuf","country":"UA","coord":{"lon":34.283333,"lat":44.549999}}
{"_id":519188,"name":"Novinki","country":"RU","coord":{"lon":37.666668,"lat":55.683334}}
The input I have is the country name and i need the id value or the country code relevant returned as a string.
I get an error:
"Type 'Any?' has no subscript members",
The method I wrote:
private func findCountryCodeBy(location: String)->String{
var result:String="";
let bundle = Bundle(for: type(of: self));
if let theURL = bundle.url(forResource: "city.list", withExtension: "json") {
do {
let data = try Data(contentsOf: theURL);
if let parsedData = try? JSONSerialization.jsonObject(with: data, options:[]) as! [String:Any] {
result = parsedData["_id"][location][0] as! String;
}
} catch {
print(error);
result = "error";
}
}
return result;
}
That is not valid JSON. I think the nearest valid JSON equivalent would be EITHER a JSON list like:
[
{"_id":707860,"name":"Hurzuf","country":"UA","coord":{"lon":34.283333,"lat":44.549999}},
{"_id":519188,"name":"Novinki","country":"RU","coord":{"lon":37.666668,"lat":55.683334}}
]
Ie, a list enclosed within square brackets with each item separated by a comma.
OR a JSON dictionary:
{
"707860": {"name":"Hurzuf","country":"UA","coord":{"lon":34.283333,"lat":44.549999}},
"519188": {"name":"Novinki","country":"RU","coord":{"lon":37.666668,"lat":55.683334}}
}
Ie, a dictionary enclosed within curly brackets with the key (in this case I've used your _id as the key) before the : and the value (a dictionary of all the other items" after the :.
(Newlines, tabs, whitespace are ignored, I've just included them to make it obvious what I've done).
I think that the dictionary version may suit your code better, but it depends on what else you want to do with the data. A list may suit some situations better.
I wrote a quick Python script to simply read JSON from a file (and not do anything else with it), and it produced a parsing error for the not-quite-JSON that you had, but it worked fine on both of my JSON examples, above.
NB: If you do NOT have control over the format of the file you are reading (ie, if you are receiving it from some other source which cannot produce it in any other format) then you will have to either modify the format of the file after you receiv it to make it valid JSON, OR you will have to use something other than JSONSerialization to read it. You could modify it by replacing all occurrences of }{ or }\n{ with },{ and then put [ at the beginning and ] at the end. That should do the job for converting this particular file to valid JSON for a list. Converting to a dictionary would be a little more involved.
Ideally though, you may have control over the file format yourself, in which case, just change whatever generates the file to produce correct JSON in the first place.
Once you have your valid JSON and parsed it into your parsedData variable, you'll then need to fix this line:
result = parsedData["_id"][location][0] as! String;
Assuming that location is the the equivalent of the _id string in the JSON, then you may be able to use the dictionary version of the JSON above and replace that line with something like:
result = parsedData[location]["country"];
However, if location is not the _id string in the JSON, then you'd be better off using the list version of the JSON above, and use a for loop to compare the values of each list item (or use a dictionary version of the JSON keyed on whatever location actually relates to in the JSON).