Swift Casting not working as expected
I have created my own protocol which I plan to use instead Any
But I don't work when I try to kick it out JSONSerialization.jsonObject
Here is my own protocol
public protocol StringOrNumber {}
extension String:StringOrNumber {}
extension NSNumber:StringOrNumber {}
extension Bool:StringOrNumber {}
extension Float:StringOrNumber {}
extension CGFloat:StringOrNumber {}
extension Int32:StringOrNumber {}
extension Int64:StringOrNumber {}
extension Int:StringOrNumber {}
extension Double:StringOrNumber {}
extension Dictionary:StringOrNumber {}
extension Array:StringOrNumber {}
extension NSDictionary:StringOrNumber {}
extension NSArray:StringOrNumber {}
extension NSString:StringOrNumber {}
extension NSNull:StringOrNumber {}
Here is the code I expect to work, but it doesn't
let json = try JSONSerialization.jsonObject(with: data, options: [])
if let object = json as? [String: StringOrNumber] {
// json is a dictionary
print(object)
}
However, if I try to convert this in 2 steps, it works like below
let json = try JSONSerialization.jsonObject(with: data, options: [])
if let object = json as? [String: Any] {
// json is a dictionary
print(object)
if let newObject:[String:StringOrNumber] = object as? [String:StringOrNumber] {
// json is a newer dictionary
print(newObject)
}
}
Here is an example JSON that I am reading from a file. ( It doesn't matter that you can try it yourself too )
{
"firstName": "John",
}
I don't understand why the first part of the code doesn't work and the second does ...
thank
source to share
it is not Swift (language) specific to Apple ...
even worse if you change the data to
let d = ["first":"john", "last":"doe", "test int": 0, "test null": NSNull()] as [String:Any]
Linux version works as expected,
["test null": <NSNull: 0x0000000000825560>, "last": "doe", "first": "john", "test int": 0]
["test null": <NSNull: 0x0000000000825560>, "last": "doe", "first": "john", "test int": 0]
["test int": 0, "last": "doe", "first": "john", "test null": <NSNull: 0x0000000000825560>]
but the print on apples
[:]
[:]
{
first = john;
last = doe;
"test int" = 0;
"test null" = "<null>";
}
It looks very strange. the following code snippet explains why
import Foundation
public protocol P {}
extension String:P {}
extension Int:P {}
extension NSNull:P {}
let d = ["first":"john", "last":"doe", "test null": NSNull(), "test int": 10] as [String:Any]
print("A)",d, type(of: d))
let d1 = d as? [String:P] ?? [:]
print("B)",d1, type(of: d1))
print()
if let data = try? JSONSerialization.data(withJSONObject: d, options: []) {
if let jobject = try? JSONSerialization.jsonObject(with: data, options: []) {
let o = jobject as? [String:Any] ?? [:]
print("1)",o, type(of: o))
var o2 = o as? [String:P] ?? [:]
print("2)",o2, type(of: o2), "is it empty?: \(o2.isEmpty)")
print()
if o2.isEmpty {
o.forEach({ (t) in
let v = t.value as? P
print("-",t.value, type(of: t.value),"as? P", v as Any)
o2[t.key] = t.value as? P ?? 0
})
}
print()
print("3)",o2)
}
}
on apple it prints
A) ["test null": <null>, "test int": 10, "first": "john", "last": "doe"] Dictionary<String, Any>
B) ["test null": <null>, "test int": 10, "first": "john", "last": "doe"] Dictionary<String, P>
1) ["test null": <null>, "test int": 10, "first": john, "last": doe] Dictionary<String, Any>
2) [:] Dictionary<String, P> is it empty?: true
- <null> NSNull as? P Optional(<null>)
- 10 __NSCFNumber as? P nil
- john NSTaggedPointerString as? P nil
- doe NSTaggedPointerString as? P nil
3) ["test null": <null>, "test int": 0, "first": 0, "last": 0]
and on linux it prints
A) ["test int": 10, "last": "doe", "first": "john", "test null": <NSNull: 0x00000000019d8c40>] Dictionary<String, Any>
B) ["test int": 10, "last": "doe", "first": "john", "test null": <NSNull: 0x00000000019d8c40>] Dictionary<String, P>
1) ["test int": 10, "last": "doe", "first": "john", "test null": <NSNull: 0x00000000019ec550>] Dictionary<String, Any>
2) ["test int": 10, "last": "doe", "first": "john", "test null": <NSNull: 0x00000000019ec550>] Dictionary<String, P> is it empty?: false
3) ["test int": 10, "last": "doe", "first": "john", "test null": <NSNull: 0x00000000019ec550>]
finally , I used slightly modified JSONSerialization source code from open source distribution (to avoid conflict with Apple Foundation, I will rename the class to _JSONSerialization :-) and change the code so that it works in my playground without any warnings and errors and ... displays the expected results :)
Why does it work now? Key
/* A class for converting JSON to Foundation/Swift objects and converting Foundation/Swift objects to JSON. An object that may be converted to JSON must have the following properties:
- Top level object is a `Swift.Array` or `Swift.Dictionary`
- All objects are `Swift.String`, `Foundation.NSNumber`, `Swift.Array`, `Swift.Dictionary`, or `Foundation.NSNull`
- All dictionary keys are `Swift.String`s
- `NSNumber`s are not NaN or infinity */
Now conditionally lowering all possible values ββto P works as expected
honestly try this snippet on linux :-) and on apple.
let d3 = [1.0, 1.0E+20]
if let data = try? JSONSerialization.data(withJSONObject: d3, options: []) {
if let jobject = try? JSONSerialization.jsonObject(with: data, options: []) as? [Double] ?? [] {
print(jobject)
}
}
apple prints
[1.0, 1e+20]
and linux
[]
and with a really great value will fail. this error comes from (in open source JSONSerialization)
if doubleResult == doubleResult.rounded() {
return (Int(doubleResult), doubleDistance)
}
replace it with
if doubleResult == doubleResult.rounded() {
if doubleResult < Double(Int.max) && doubleResult > Double(Int.min) {
return (Int(doubleResult), doubleDistance)
}
}
and "deserialization" works as expected (serialization has other errors ...)
source to share
If you just want to check protocol compliance:
Use is
insteadas
if json is [String : StringOrNumber] {
print("valid")
}
else {
print("invalid")
}
If you want to use the converted type:
if let object = (json as? [String: Any]) as? [String : StringOrNumber] {
print("valid object = \(object)")
}
else {
print("invalid")
}
source to share