How to format a JSON String in Swift? - json

I have a JSON String that follows a format like { name: "John" } and not { "name" : "John"} and that is resulting in a nil whenever I try to access the name key because:
Error Domain=NSCocoaErrorDomain Code=3840 "No string key for value in object around character 1."
I am looking for a function that fixes/parses/formats this JSON file into something readable? How do sites like JSON Format do it?

Funny enough, { name: "John" } makes a valid JSON object in Javascript. So your problem now becomes finding a Javascript intepreter for Swift!
There's one built-in to recent versions of Mac OS X and iOS: WKWebView. It's a web rendering engine with a Javascript parser. Link your target with WebKit and try this:
import WebKit
class MyJSONParser {
private static let webView = WKWebView()
class func parse(jsonString: String, completionHandler: (AnyObject?, NSError?) -> Void) {
self.webView.evaluateJavaScript(jsonString, completionHandler: completionHandler)
}
}
Usage:
let str = "{ firstName: 'John', lastName: 'Smith' }"
// You must assign the JSON string to a variable or the Javascript
// will return void. Note that this runs asynchronously
MyJSONParser.parse("tmp = \(str)") { result, error in
guard error == nil else {
print(error)
return
}
if let dict = result as? [String: String] {
print(dict)
} else {
print("Can't convert to Dictionary")
}
}
Swift 3
import WebKit
class MyJSONParser {
private static let webView = WKWebView()
class func parse(jsonString: String, completionHandler: #escaping (Any?, Error?) -> Void) {
self.webView.evaluateJavaScript(jsonString, completionHandler: completionHandler)
}
}
let str = "{ firstName: 'John', lastName: 'Smith' }"
// You must assign the JSON string to a variable or the Javascript
// will return void. Note that this runs asynchronously
MyJSONParser.parse(jsonString: "tmp = \(str)") { result, error in
guard error == nil else {
print(error!)
return
}
if let dict = result as? [String: String] {
print(dict)
} else {
print("Can't convert to Dictionary")
}
}

Related

Error at generic helper function for reading JSON from an URL

In one of my projects, I want to read JSON from several URLs in several Views into several structs so I decided to write a small, but generic helper function.
This function should be called e.g.
View 1:
let call1 = Bundle.main.decode(iobrokerSection.self, from: "http://192.168.1.205:8087/get/javascript.0.Fahrzeiten.Dauer")
View 2:
let call2 = Bundle.main.decodeURL(iobrokerweather.self, from: "http://192.168.1.205:8087/get/javascript.0.Fahrzeiten.Weather")
and so on.
For the first example the struct iobrokerSection is
struct iobrokerNumberDataPoint: Codable {
var val: Int
var ack: Bool
var ts: Int
var q: Int
var from: String
var user: String
var lc: Int
var _id: String
var type: String
}
And here is my helper function
extension Bundle {
func decodeURL<T: Decodable>(_ type: T.Type, from urlString: String) -> T {
guard let url = URL(string: urlString) else {
fatalError("Placeholder for a good error message")
}
let request = URLRequest(url: url)
URLSession.shared.dataTask(with: request) { data, response, error in
guard let loaded = try? JSONDecoder().decode(T.self, from: data!) else {
fatalError("Placeholder for a good error message")
}
}.resume()
return loaded
}
}
I think that I understand why I'm getting the compiler message "Cannot convert return expression of type Bool to return Type T" at "return loaded".
But I don't have any idea how to fix this.
May anyone give me a hint?
First it is Swift naming convention to name all your structures, classes and protocols starting with an uppercase letter. Second you can't wait for an asynchronous method to finish to return a value. You need to add a completion handler to your method. Third you don't need to use a URLRequest if your intent is only to get some data. You can just use an URL and pass a URL to your method instead of a string. Fourth don't force unwrap the returned data it might be nil. You need to safely unwrap your optional data and in case of error pass it to the completion handler. Your decode method should look like something like this:
extension Bundle {
func decode<T: Decodable>(_ type: T.Type, from url: URL, completion: #escaping (T?, Error?) -> Void) {
URLSession.shared.dataTask(with: url) { data, response, error in
guard let data = data else {
completion(nil, error)
return
}
do {
try completion(JSONDecoder().decode(T.self, from: data), nil)
} catch {
completion(nil, error)
}
}.resume()
}
}
When calling this method you need to get the asynchronous result inside the resulting closure:
struct IOBrokerNumberDataPoint: Codable {
var val: Int
var ack: Bool
var ts: Int
var q: Int
var from: String
var user: String
var lc: Int
var id: String
var type: String
enum CodingKeys: String, CodingKey {
case val, ack, ts, q, from, user, lc, id = "_id", type
}
}
let url = URL(string: "http://192.168.1.205:8087/get/javascript.0.Fahrzeiten.Dauer")!
Bundle.main.decode(IOBrokerNumberDataPoint.self, from: url) { brokerNumberDataPoint, error in
guard let brokerNumberDataPoint = brokerNumberDataPoint else {
print("error", error ?? "")
return
}
print("brokerNumberDataPoint", brokerNumberDataPoint)
// use brokerNumberDataPoint here
}
Another option is to use Swift 5 Result generic enumeration.
extension Bundle {
func decode<T: Decodable>(from url: URL, completion: #escaping (Result<T, Error>) -> Void) {
URLSession.shared.dataTask(with: url) { data, response, error in
guard let data = data, error == nil else {
if let error = error { completion(.failure(error)) }
return
}
do {
try completion(.success(JSONDecoder().decode(T.self, from: data)))
} catch {
completion(.failure(error))
}
}.resume()
}
}
Usage:
let url = URL(string: "http://192.168.1.205:8087/get/javascript.0.Fahrzeiten.Dauer")!
Bundle.main.decode(from: url) { (result: Result<IOBrokerNumberDataPoint, Error>) in
switch result {
case let .success(brokerNumberDataPoint):
print("brokerNumberDataPoint", brokerNumberDataPoint)
// use brokerNumberDataPoint here
case let .failure(error):
print("error:", error)
}
}

Get user ID from JWT (JSON web token)

I am using the plugin to authenticate WordPress using api-rest : JWT Authentication for WP REST API
From the request to the server I get the following answer:
{
"token": "eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJpc3MiOiJodHRwczpcL1wvbWlob3N0Lm9yZ1wvcHJ1ZWJhcyIsImlhdCI6MTU1MzcyNDM4MSwibmJmIjoxNTUzNzI0MzgxLCJleHAiOjE1NTQzMjkxODEsImRhdGEiOnsidXNlciI6eyJpZCI6IjIifX19.rgi5Q2c8RCoHRp-lJiJN8xQaOavn9T_q8cmf8v1-57o",
"user_email": "abc#test.com",
"user_nicename": "test",
"user_display_name": "Test"
}
So far everything works fine, but I need to know the user ID.
I have read that the token is coded in base64 and within this is the ID. Trying to decode, I see if the ID that I need is there.
In swift with this function I decode the token, but I can not get the dictionary ID.
func decode(_ token: String) -> [String: AnyObject]? {
let string = token.components(separatedBy: ".")
let toDecode = string[1] as String
var stringtoDecode: String = toDecode.replacingOccurrences(of: "-", with: "+") // 62nd char of encoding
stringtoDecode = stringtoDecode.replacingOccurrences(of: "_", with: "/") // 63rd char of encoding
switch (stringtoDecode.utf16.count % 4) {
case 2: stringtoDecode = "\(stringtoDecode)=="
case 3: stringtoDecode = "\(stringtoDecode)="
default: // nothing to do stringtoDecode can stay the same
print("")
}
let dataToDecode = Data(base64Encoded: stringtoDecode, options: [])
let base64DecodedString = NSString(data: dataToDecode!, encoding: String.Encoding.utf8.rawValue)
var values: [String: AnyObject]?
if let string = base64DecodedString {
if let data = string.data(using: String.Encoding.utf8.rawValue, allowLossyConversion: true) {
values = try! JSONSerialization.jsonObject(with: data, options: JSONSerialization.ReadingOptions.allowFragments) as? [String : AnyObject]
}
}
return values
}
The dictionary that returns this function is:
["iss": https://myhost.me/test, "exp": 1554235730, "nbf": 1553630930, "iat": 1553630930, "data": {
user = {
id = 2;
};
}]
How do I get the ID from this dictionary?
Your code is pretty unswifty.
Basically don't use NS... classes in Swift if there is a native equivalent and a JSON dictionary is always value type ([String:Any]).
I recommend to add an Error enum, make the function can throw, decode the serialized token with Decodable and return the Token instance on success
struct Token : Decodable {
let data : UserData
struct UserData : Decodable {
let user : User
struct User : Decodable {
let id : String
}
}
}
You are encouraged to keep the parameter label in the method declaration
enum TokenError : Error {
case invalidJWTFormat, invalidBase64EncodedData
}
func decode(token: String) throws -> Token {
let components = token.components(separatedBy: ".")
guard components.count == 3 else { throw TokenError.invalidJWTFormat }
var decodedString = components[1]
.replacingOccurrences(of: "-", with: "+")
.replacingOccurrences(of: "_", with: "/")
while decodedString.utf16.count % 4 != 0 {
decodedString += "="
}
guard let decodedData = Data(base64Encoded: decodedString) else { throw TokenError.invalidBase64EncodedData }
return try JSONDecoder().decode(Token.self, from: decodedData)
}
and call it
do {
let userID = try decode(token: "eyJ0eXAi.....").data.user.id
} catch { print(error) }

Decoding JSON strings containing arrays

I'm encoding and decoding to and from JSON strings using JSONSerialization in the class below. I can encode both NSDictionaries & NSArrays and I can decode strings that have been encoded using NSDictionaries but not strings that were encoded from arrays, it barfs at JSONSerialization.jsonObject( ...
I can work without arrays, at a pinch but it would be nice to know why this is happening. Thoughts appreciated
let a = [1,2,3,4,5]
let s = JSON.Encode( a )!
JSON.Decode( s ) // fails
let a = ["a" : 1, "b" : 2, "c" : 3 ]
let s = JSON.Encode( a )!
JSON.Decode( s ) // works
-
class JSON: NSObject {
static func Encode( _ obj: Any ) -> String? {
do {
let data = try JSONSerialization.data(withJSONObject: obj, options:JSONSerialization.WritingOptions(rawValue: 0) )
if let string = NSString(data: data, encoding: String.Encoding.utf8.rawValue) {
return string as String
}
return nil
} catch let error as NSError {
return nil
}
}
static func Decode( _ s: String ) -> (NSDictionary?) {
let data = s.data(using: String.Encoding.utf8, allowLossyConversion: false)!
do {
// fails here to work on anything other than "{ a : b, c : d }"
// hates any [1,2,3] arrays in the string
let json = try JSONSerialization.jsonObject(with: data, options:JSONSerialization.ReadingOptions.mutableContainers) as! NSDictionary
return json
} catch let error as NSError {
return nil
}
}
}
You are casting the result to dictionary so it cannot work with something else.
A solution is to make the function generic and specify the expected return type.
I cleaned up the code a bit, basically it’s recommended to make a function throw if it contains a throwing API.
class JSON {
enum JSONError : Error { case typeMismatch }
static func encode( _ obj: Any ) throws -> String {
let data = try JSONSerialization.data(withJSONObject: obj)
return String(data: data, encoding: .utf8)!
}
static func decode<T>( _ s: String ) throws -> T {
let data = Data(s.utf8)
guard let result = try JSONSerialization.jsonObject(with: data) as? T else {
throw JSONError.typeMismatch
}
return result
}
}
let a = [1,2,3,4,5]
do {
let s = try JSON.encode(a)
let u : [Int] = try JSON.decode(s)
print(u)
} catch { print(error) }
Note (as always):
In Swift do not use NSDictionary, NSString and NSErrorand .mutableContainers is completely pointless.

Vapor JSON from `[String: Any]` Dictionary

If I build a Swift dictionary, i.e. [String: Any] how can I return that as JSON? I tried this, but it gives me the error: Argument labels '(node:)' do not match any available overloads.
drop.get("test") { request in
var data: [String: Any] = [:]
data["name"] = "David"
data["state"] = "CA"
return try JSON(node: data)
}
Convoluted as heck, but this allows you to use [String:Any].makeNode(), as long as the internals are NodeRepresentable, NSNumber based, or NSNull :) --
import Node
enum NodeConversionError : LocalizedError {
case invalidValue(String,Any)
var errorDescription: String? {
switch self {
case .invalidValue(let key, let value): return "Value for \(key) is not NodeRepresentable - " + String(describing: type(of: value))
}
}
}
extension NSNumber : NodeRepresentable {
public func makeNode(context: Context = EmptyNode) throws -> Node {
return Node.number(.double(Double(self)))
}
}
extension NSString : NodeRepresentable {
public func makeNode(context: Context = EmptyNode) throws -> Node {
return Node.string(String(self))
}
}
extension KeyAccessible where Key == String, Value == Any {
public func makeNode(context: Context = EmptyNode) throws -> Node {
var mutable: [String : Node] = [:]
try allItems.forEach { key, value in
if let _ = value as? NSNull {
mutable[key] = Node.null
} else {
guard let nodeable = value as? NodeRepresentable else { throw NodeConversionError.invalidValue(key, value) }
mutable[key] = try nodeable.makeNode()
}
}
return .object(mutable)
}
public func converted<T: NodeInitializable>(to type: T.Type = T.self) throws -> T {
return try makeNode().converted()
}
}
With that header you can:
return try JSON(node: data.makeNode())
JSON cannot be initialized from a [String : Any] dictionary because Any is not convertible to Node.
There are only a limited number of types that Node can be. (See Node source). If you know your objects are all going to be the same type, use a dictionary that only allows that type. So for your example, [String : String].
If you're going to be getting data from the request, you can try using request.json as is used in the documentation here.
EDIT:
Another (possibly better) solution would be to make your dictionary [String: Node] and then you can include any type that conforms to Node. You may have to call the object's makeNode() function to add it to the dictionary though.

Alamofire - serializing generic response objects to model from JSON without forced unwrap

I'm trying to serialize as request response (in the form of JSON) to a model - the JSON maybe missing some properties sometimes hence I've tried to use the method here to protect against that.
However, the model is throwing the classic fatal error: unexpectedly found nil while unwrapping an Optional value from the model - where the model has tried to force unwrap a non-optional value on the init call from the responseObject JSON - but because it doesn't exist, the force unwrap fails.
What is the best solution for handling this from Alamofire?
Model:
final public class User: ResponseObjectSerializable {
let username: String
let name: String
public init?(response: NSHTTPURLResponse, representation: AnyObject) {
self.username = representation.valueForKeyPath("username") as! String
self.name = representation.valueForKeyPath("name") as! String
}
}
Alamofire request extension:
import Foundation
import Alamofire
public protocol ResponseObjectSerializable {
init?(response: NSHTTPURLResponse, representation: AnyObject)
}
extension Request {
public func responseObject<T: ResponseObjectSerializable>(completionHandler: Response<T, NSError> -> Void) -> Self {
let responseSerializer = ResponseSerializer<T, NSError> { request, response, data, error in
guard error == nil else { return .Failure(error!) }
let JSONResponseSerializer = Request.JSONResponseSerializer(options: .AllowFragments)
let result = JSONResponseSerializer.serializeResponse(request, response, data, error)
switch result {
case .Success(let value):
if let
response = response,
responseObject = T(response: response, representation: value)
{
return .Success(responseObject)
} else {
let failureReason = "JSON could not be serialized into response object: \(value)"
let error = Error.errorWithCode(.JSONSerializationFailed, failureReason: failureReason)
return .Failure(error)
}
case .Failure(let error):
return .Failure(error)
}
}
return response(responseSerializer: responseSerializer, completionHandler: completionHandler)
}
}
Client request:
Alamofire.request(.GET, requestUrl, parameters: params, encoding: .URL)
.validate()
.responseObject { (response: Response<User, NSError>) in
switch response.result {
case .Success(let user):
completionHandler(user, nil)
case .Failure(let error):
print("Request failed with error: \(error)")
completionHandler(nil, error)
}
}
return self
You have your User class with a failable initializer, but you don't appear to be using it.
Here's a simple version of what I'm thinking:
class User {
let firstName: String
init?(name: String?) {
guard let name = name else {
self.firstName = "" // trash value needed until swift 2.2
return nil
}
self.firstName = name
}
}