I get a response from an API (unfortunately, I cannot change it) that looks something like (just an example):
As bytes => "{\"key\":\"value\"}"
The starting and ending quotes and the escaped quotes are all part of the response, I am solving it in a really ugly way that looks like this:
// (...) Receiving response
guard var responseString = String(bytes: data, encoding: .utf8) else {
print("Response wasn't just a string, great!") // unfortunately, this never happens
return
}
responseString = responseString.trimmingCharacters(in: .whitespacesAndNewlines) // make sure it is trimmed
responseString = String(responseString.dropFirst()) // drop the quote at the start
responseString = String(responseString.dropLast()) // drop the quote at the end
responseString = responseString.replacingOccurrences(of: "\\\"", with: "\"")// convert all \" to " (and hope nothing else is escaped <<< this is really bad!!!)
let responseDataToDecode = responseString.data(using: .utf8)!
// (...) decoding with JSONDecoder
Is there a way to automatically unescape the string and use the JSON object that is contained in it?
If it's double-encoded, then you just need to double-decode. If I understand correctly, the incoming data is like this:
let str = #""{\"key\":\"value\"}""#
// "{\\"key\\":\\"value\\"}"
The first byte is ", the second byte is {, the third byte is \, the fourth byte is ".
That's a JSON-encoded string. So decode that as a string (there was a time when this didn't work because it's a "fragment," but it works fine currently, at least in all my tests):
let decoder = JSONDecoder()
let string = try! decoder.decode(String.self, from: Data(str.utf8)) // {"key":"value"}
And then decode that as your type ([String:String] just for example):
let result = try! decoder.decode([String:String].self, from: Data(string.utf8))
// ["key": "value"]
(IMO this kind of double-encoding is fine, BTW, and I'm not sure why there are so many comments against it. Serializing an arbitrary object makes a lot more sense in many cases than forcing the schema to deal with an arbitrary structure. As long as it's cleanly encoded, I don't see any problem here.)
There's a first step: You need an official documented statement what exactly the format of your data is. It looks like someone took some data, turned it into JSON data, interpreted the data as a string, and then converted the string to a JSON fragment. It's not difficult to decode the JSON fragment, getting a string, turning the string into data, and decoding that data as JSON (starting with JSONSerialization and .allowFragments, probably the only time you should use .allowFragments in your life). Doing it without swearing is hard.
But first you want in writing that this is actually the format. Because I would bet that whoever is responsible for that data format will eventually change it without telling you and break your code.
Related
First off, what do we call a dictionary with a format like this in iOS?
(
{
name = "Apple";
value = "fruit-1";
},
{
name = "Banana";
value = "fruit-2";
}
)
And for my main question. I somehow need to format a string of JSON, like this:
[{"name":"Apple","value":"fruit-1"},{"name":"Banana","value":"fruit-2"}]
into whatever that format is called (of the string above).
For context, the existing approach of my project uses CoreData where the Server response (which uses the mystery format above) gets saved locally as a String, and I want to follow that format.
EDIT: for more context, I really need to just get the first format into the database because a module of a project was built to read the data with that format (e.g. make use of NSString.propertyList()).
Using a library called ios hierarchy viewer, I can see the saved object in the device.
Original format, server json to db (core data) in Objective-C:
What I've been trying to do in Swift, server json to local using JSONSerialization:
First off, what do we call a dictionary with a format like this in iOS?
According to the documentation of NSString.propertyList(), that's a "text representation of a property list".
It's a wonky, non-standard pretty-printing obtained by calling NSArray.description or NSDictionary.description.
Here's an example that shows a round-trip of data:
// The opening `{` indentation is fucky, but that's how it's generated.
let inputPropertyList = """
(
{
name = "Apple";
value = "fruit-1";
},
{
name = "Banana";
value = "fruit-2";
}
)
"""
// The result is an `Any` because we don't know if the root structure
// of the property list is an array or a dictionary
let deserialized: Any = inputPropertyList.propertyList()
// If you want the description in the same format, you need to cast to
// Foundation.NSArray or Foundation.NSDictionary.
// Swift.Array and Swift.Dictionary have a different description format.
let nsDict = deserialized as! NSArray
let roundTrippedPropertyList = nsDict.description
print(roundTrippedPropertyList)
assert(roundTrippedPropertyList == inputPropertyList)
The second format you show is what you get when you display an object in the debug console. That's the output of the object's description property. It isn't a "JSON string", exactly.
If you want to convert your objets to a true JSON string, see below.
As Alexander pointed out, the first string in your question is the output from NSString's propertyList() function. The format looks quite similar to "pretty-printed" JSON, but it's different enough that it it won't work that way.
The `propertyList() function is a debugging-only function, and I don't know of an existing way to parse that back into objects. If that is the string that's being sent by your server, your server is broken. If that's what you see in core data when you log the contents of a field, it's probably a misunderstanding on your part.
To convert an object to pretty JSON, see this answer, where I created an extension to the Encodable format that implements a property "prettyJSON":
extension Encodable {
var prettyJSON: String {
let encoder = JSONEncoder()
encoder.outputFormatting = .prettyPrinted
guard let data = try? encoder.encode(self),
let output = String(data: data, encoding: .utf8)
else { return "Error converting \(self) to JSON string" }
return output
}
}
That should work for any object that supports the Encodable protocol. (And your object should.)
or can I check if a number was decoded as a decimal number and not and integer later?
if let int = any as? Int {
print("Object in an integer")
} else if let num = any as? Double {
print("Object in a double")
}
, where "any" is an Any value and = 1.0 (not a string) in the JSON file. "any" can be cast to both integer and double (so the order of which I check determines the outcome), but I would like to keep the original format from the JSON file.
Decoding is done using the following line:
let json = try JSONSerialization.jsonObject(with: data, options: [])
Edit: I've tried checking CFType, but get the same for both 1 and 1.0 (inspired by http://stackoverflow.com/a/30223989/1694526)
Any ideas?
As already mentioned by #Sulthan this is not possible on the level you are working as JSONSerialization will and should use a single class to represent a value and may not determine its type.
You could try finding some other tool to check for values but does it really make sense?
You are trying to look for differences between Int and Double but what about 64 or 32 bit? Or signed and unsigned? We usually don't write those into strings so there really is no way to distinguish between them. So there is really no general logic in doing so.
Are you sure the returned JSON will always have ".0" appended for these values? This really depends on the system and a smallest optimization would trim that because JSON standard does not include precisions on numbers. For instance if I use JSONSerialization and print out String(data: (try! JSONSerialization.data(withJSONObject: [ "value": 1.0 ], options: .prettyPrinted)), encoding: .utf8) I receive: {\n \"value\" : 1\n} which means it trimmed ".0" anyway.
I find it hard to understand how this would be good structurally. If you need to save these data for instance into your database you will need to define the size and type of the primitive to hold your data. If you need to use some arithmetics you again need to specify the type...
The only way would be to use it as a display string. But in that case your value should be returned as a string and not as a number.
The solution is to parse to an NSNumber and then to a Decimal (or NSDecimalNumber). DO NOT parse via a Double.
let jsonString = "[ 4.01 ]"
let jsonData = jsonString.data(using: .utf8)!
let jsonArray = try! JSONSerialization.jsonObject(with: jsonData, options: []) as! [Any]
// This is the WRONG way to parse decimals (via a Double)
// parseAttemptA = 4.009999999999998976
let parseAttemptA: Decimal = Decimal(jsonArray[0] as! Double)
// This is the CORRECT way to parse decimals (via an NSNumber)
// parseAttemptB = 4.01
let parseAttemptB: Decimal = (jsonArray[0] as! NSNumber).decimalValue
Here's a screenshot of a playground...
Let's say there's a JSON-string I got from server: "\"3\"" (with quotes, i.e. length == 3 here)
In Android-world, I can do:
gson.fromJson(json, new TypeToken<String>() {}.getType()); - it returns "3" (i.e. length == 1)
In C#-world, can use NewtonSoft.Json:
JsonConvert.DeserializeObject<string>(json, settings) - it returns "3" (i.e. length == 1)
And other way around, I do have string I want to serialize as a JSON.
In Android I'd do gson.toJson("\"3\"") and in C# - JsonConvert.SerializeObject("\"3\"")
The problem with JSONSerialization is that it doesn't treat plain string as a valid JSON: JSONSerialization.isValidJSONObject("\"3\"") == *false*
What would be equivalent in Swift / Obj-C world?
The ugly workaround I've found (except of just adding/removing quotes) so far is to wrap string into 1-item-array to make JSONSerialization happy and then remove "[","]" from resulted JSON-string (and other way around - add "[", "]" before deserialization), but it's a way too disgusting to be the real solution for this problem.
When de-serializing JSON which does not have an array or
dictionary as top-level object you can pass the
.allowFragments option:
let jsonString = "\"3\""
let jsonData = jsonString.data(using: .utf8)!
let json = try! JSONSerialization.jsonObject(with: jsonData, options: .allowFragments)
if let str = json as? String {
print(str) // 3
}
However, there seems to be no way to serialize a plain string to JSON
with the JSONSerialization class from the Foundation library.
Note that according to the JSON specification,
a JSON object is a collection of name/value pairs (dictionary) or
an ordered list of values (array). A single string is not a valid
JSON object.
Recently, I am doing a NSUrlSession task to upload multiple images to the backend . I appended all the images in .png representation in an array. I converted the whole array into a base64 String format and tried to send the whole body as a string.
Conversion of imageArray to String -
let imageArrayData: NSData = NSKeyedArchiver.archivedDataWithRootObject(imageArray)
let imageArrayBase64String = imageArrayData.base64EncodedStringWithOptions(NSDataBase64EncodingOptions(rawValue: 0))
Here printing the Array ,I get -
(String) imageArrayBase64String = "YnBsaXN0MDDUAQIDBAUIIiNUJHRvcFgkb2JqZWN0c1gkdmVyc2lvblkkYXJjaGl2ZXLRBgdUcm9vdIABpQkKEBQcVSRudWxs0gsMDQ5WJGNsYXNzWk5TLm9iamVjdHOABKEPgALSEQsSE1dOUy5kYXRhTxIBe8QxiVBORw0KGgoAAAANSUhEUgAAEMAAAAsgCAIAAABnC3DjAAABGWlDQ1BJQ0MgUHJvZmlsZQAAKJFjYGBSSCwoyGESYGDIzSspCnJ3UoiIjFJgf8DAzCDDwMUgwGCUmFxc4BgQ4MMABDAaFXy7xsAIoi/rgszClMcLuFJSi5OB9B8gzk4uKCphYGDMALKVy0sKQOweIFskKRvMXgBiFwEdCGRvAbHTIewTYDUQ9h2wmpAgZyD7A5DNlwRmM4Hs4kuHsAVAbKi9ICDomJKflKoA8r2GoaWlhSaJfiAISlIrSkC0c35BZVFmekaJgiMwpFIVPPOS9XQUjAwMzRgYQOEOUf05EByejGJnEGIIgBCbI8HA4L+UgYHlD0LMpJeBYYEOAwP/VISYmiEDg4A+A8O+OcmlRWVQYxiZjBkYCPEBNDZKYi1QenUAAAAcaURPVAAAAAIAAAAAAAAFkAAAACgAAAWQAAAFkADAfM/Go8QlAABAAElEQVR4AXS8BXQcV7b3K0Pm3jt3IIlBlmzLFjMzMzMztVhqUasZ1ChmZibLsixmmSmGkB1yzIyhgZubTGJ//+rK6GW97721/muvffbZdaqqq7vqVNX+tUJijjZEKTBIztNNyNZKoeol5WlB8dnqibmaKfk68BOy1JNztVKpOkk5mgiSItPkPjLVU/K14rIOpxboJlO1YzNVE3I0IDJzayiMhmBclhoisEhOK9JHPhZPytNILdCiFGqn07STqKrJVI2UfM24rEPx2YcRzKDppRZoYxWkknM1IGxPSp52cp56epFOar5mClUjmYoN1kor0kVyWpFOejHG18E4KQXqqYUaacUaGSVa2XSdPJY+pUAtk6aVxdB"
Creating the body -
let body = "task=doNotification&select_category=\(selectCategory!)&select_type=\(selectType!)&class=\(classid!)&repliable=\(repliable)&select_students=\(selectedStudents)&select_group=\(selectGroup!)&title=\(SbjctOrTtlTxtFld.text!)&text=\(textVieww.text!)&image=\(imageArrayBase64String)&date=\(dateText!)&time=\(timeText!)"
Here printing the body,I get -
(String) body = "task=doNotification&select_category=exams&select_type=check&class=2&repliable=1&select_students=(\n 26,\n 25\n)&select_group=11&title=self&text=Adam <IMG_0002>&image=YnBsaXN0MDDUAQIDBAUIIiNUJHRvcFgkb2JqZWN0c1gkdmVyc2lvblkkYXJjaGl2ZXLRBgdUcm9vdIABpQkKEBQcVSRudWxs0gsMDQ5WJGNsYXNzWk5TLm9iamVjdHOABKEPgALSEQsSE1dOUy5kYXRhTxIBe8QxiVBORw0KGgoAAAANSUhEUgAAEMAAAAsgCAIAAABnC3DjAAABGWlDQ1BJQ0MgUHJvZmlsZQAAKJFjYGBSSCwoyGESYGDIzSspCnJ3UoiIjFJgf8DAzCDDwMUgwGCUmFxc4BgQ4MMABDAaFXy7xsAIoi/rgszClMcLuFJSi5OB9B8gzk4uKCphYGDMALKVy0sKQOweIFskKRvMXgBiFwEdCGRvAbHTIewTYDUQ9h2wmpAgZyD7A5DNlwRmM4Hs4kuHsAVAbKi9ICDomJKflKoA8r2GoaWlhSaJfiAISlIrSkC0c35BZVFmekaJgiMwpFIVPPOS9XQUjAwMzRgYQOEOUf05EByejGJnEGIIgBCbI8HA4L+UgYHlD0LMpJeBYYEOAwP/VISYmiEDg4A+A8O+OcmlRWVQYxiZjBkYCPEBNDZKYi1QenUAAAAcaURPVAAAAAIAAAAAAAAFkAAAACgAAAWQAAAFkADAfM/Go8QlAABAAElEQVR4AXS8BXQcV7b3K0Pm3jt3IIlBlmzLFjMzMzMztVhqUasZ1ChmZibLsixmmSmGkB1yzIyhgZubTGJ//+rK6GW97721/muvffbZdaqqq7vqVNX+tUJijjZEKTBIztNNyNZKoeol5WlB8dnqibmaKfk68BOy1JNztVKpOkk5mgiSItPkPjLVU/K14rIOpxboJlO1YzN"
As you can notice that the body don't even pass the full encoded string of the array and it also is not passing the last 2 parameters i.e date & time.
So why is it so ?
Calling the web service -
func sendAPIRequest(urlpath:NSString,body: NSString , completion: (result: NSMutableDictionary, error: AnyObject?)-> Void ) -> Void
{
let url:NSURL = NSURL(string: urlpath as String)!
let request = NSMutableURLRequest(URL: url)
request.HTTPMethod = "POST"
let bodydata = body.dataUsingEncoding(NSUTF8StringEncoding)
request.HTTPBody = bodydata
let config = NSURLSessionConfiguration.defaultSessionConfiguration()
let session = NSURLSession(configuration: config)
let task = session.dataTaskWithRequest(request) { (data, response, error) -> Void in
do
{
let resultdic = try NSJSONSerialization.JSONObjectWithData(data!, options: NSJSONReadingOptions.MutableContainers) as? NSMutableDictionary
//print(resultdic!)
completion(result: resultdic!,error: nil)
}
catch
{
print("error")
}
}
task.resume()
A couple of observations:
It looks like you're looking at body in the debugger. That will truncate the string (though usually it shows ellipses at the end to indicate that it was truncated). I'd suggest you try print(body) and see if you see the whole string there.
Your selectStudents is an NSArray and when you do string interpolation on that, it includes a newline character in it. The net effect is that this x-www-form-urlencoded request is not well-formed and will likely be rejected by the server. There are a couple of ways of resolving with this
If your server is really expecting a string of the form select_students=(\n 26,\n 25\n) (which I'd be very surprised if that's really what you want), you'd percent escape that string:
let selectedStudentsString = "selectedStudents=\(selectedStudents)".stringByAddingPercentEncodingWithAllowedCharacters(NSCharacterSet.URLQueryParameterAllowedCharacterSet())!
and that will yield:
selectedStudents=(%0A%20%20%20%20foo,%0A%20%20%20%20bar%0A)
where URLQueryParameterAllowedCharacterSet is defined as:
extension NSCharacterSet {
/// Returns the character set for characters allowed in the individual parameters within a query URL component.
///
/// The query component of a URL is the component immediately following a question mark (?).
/// For example, in the URL `http://www.example.com/index.php?key1=value1#jumpLink`, the query
/// component is `key1=value1`. The individual parameters of that query would be the key `key1`
/// and its associated value `value1`.
///
/// According to RFC 3986, the set of unreserved characters includes
///
/// `ALPHA / DIGIT / "-" / "." / "_" / "~"`
///
/// In section 3.4 of the RFC, it further recommends adding `/` and `?` to the list of unescaped characters
/// for the sake of compatibility with some erroneous implementations, so this routine also allows those
/// to pass unescaped.
class func URLQueryParameterAllowedCharacterSet() -> Self {
return self.init(charactersInString: "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789-._~/?")
}
}
See https://stackoverflow.com/a/35912606/1271826 for alternative percent-escaping options. But avoid using stringByAddingPercentEncodingWithAllowedCharacters with any of the existing character sets (e.g. URLQueryAllowedCharacterSet), because they all allow reserved characters to pass unescaped.
If you really want this received as an array in an x-www-form-urlencoded request by your server code, you'd probably want:
var selectedStudentsArray = [String]()
for index in 0 ..< selectedStudents.count {
selectedStudentsArray.append("selectedStudents[]=\(selectedStudents[index])")
}
let selectedStudentsString = selectedStudentsArray.joinWithSeparator("&")
and that will yield:
selectedStudents[]=26&selectedStudents[]=25
That's the right way to send an array of values in an x-www-form-urlencoded request.
Your text value includes reserved characters, too. You should be percent escaping this (and anything else that might include characters outside of a-z, A-Z, 0-9, and -, ., _, ~, /, and ?). Again, when doing x-www-form-urlencoded request like this, all reserved characters must be percent escaped.
You are taking your array of images and building a NSKeyedArchiver. This is going to be one big, opaque, blob object as far as your web service is concerned. If your intent is to treat this as a single blob on the server, that's fine, but if you were hoping to then extract the images out of that, you're making your life unnecessarily complicated.
You can either:
Base-64 encode the images separately and then use the same format I suggested for selectStudents, e.g.
image[]=Bsa...JJH&image[]=5YJ...mVj
Note, you should use the original contents of the data (e.g. get the NSData directly from the original file/asset, avoiding UIImage altogether) or, if you really need to round-trip it through a UIImage (which I'd discourage if you can prevent it), then use UIImagePNGRepresentation or UIImageJPEGRepresentation to create a new NSData from the UIImage.
You might consider using a multipart/form-data request and then the individual images (again, with the aforementioned comment about getting the NSData notwithstanding) can be stored individually. If you do proper multipart/form-data request, it avoids needing to do any percent-escaping anywhere, too, avoids base-64 encoding (which makes the transmission 33% larger than it needs to be), etc.
Bottom line, properly creating these sorts of requests is very, very complicated, and unless you really want to unravel all of these idiosyncrasies, I'd really encourage you to consider Alamofire, which gets you out of these weeds. And if you truly feel compelled to reinvent the wheel, I'd suggest you research these topics individually, as this is far too broad for a single question. Each of my points above probably warrants a separate question.
I have JSONString from api as bellow:
[JSONString from api]
But after I read in iOS from Alamofire the order of the JSONString is not correct as api:
[JSON After read in iOS]
How can I keep the JSON format the same order as api?
As explained by #Nimit, JSON format represented in your callback and the API response is of least concern. What you need to care about is that when you are accessing the values from the response, the KEY should be same as seen in the API. No mismatch, not even of the case-sensitive letter, or you will always get the NIL in the response.
To explain it better to you with the use of Alamofire, let's me show you one example:
let APIURL = "https://api.yoururl.com"
Alamofire.request(.GET, APIURL , headers: headers) .responseJSON { response in
let value = response.result.value!
let JSONRes = JSON(value)
let KLValue = JSONRes["Kuala Lumpur"].int!
print(KLValue) //Or call a function to handle the callback
}
Here I am using SwiftyJSON for JSON. In the end, all you want to do is get the data out of the associated keys in the JSON response, no need to worry about how they have been formatted, or what's the order of Keys in the response - most of the time you will get the same as in the API - but in case it changes, need not to worry.
On the another front, to be sure that nothing happens to your app when JSON fields are nil, always put an if-let like this:
if let valueFromJSON = JSONRes["Kuala Lumpur"].string {
someVariable = valueFromJSON
} else {
someVariable = "No Value"
}
Thanks!
You can't do it, unless you write your own JSON parser. Any self-respecting JSON library won't guarantee you the order, if it wants to conform to the JSON spec.
From the definition of the JSON object:
the NSDictionary class represents an unordered collection of objects;
however, they associate each value with a key, which acts like a label
for the value. This is useful for modeling relationships between pairs
of objects.
If you have a jsonObject, such as data, then you can convert to json string like this:
let jsonString = JSONSerialization.data(withJSONObject: data,
options: JSONSerialization.WritingOptions.sortedKeys)
when you use sortedKeys option, the json will be specifies that the output sorts keys in lexicographic order.