3

I have created a custom protocol,which i am planning to use in place of Any

But i doesn't work when i try to cast it from JSONSerialization.jsonObject

Here is my custom 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 which 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 as 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 the sample JSON i am reading from file.(Doesn't matter you can try your own too)

{
     "firstName": "John",
}

I don't understand why the first piece of code doesn't works and 2nd one does ...

Thanks

Hamish
  • 78,605
  • 19
  • 187
  • 280
Mihir Mehta
  • 13,743
  • 3
  • 64
  • 88

2 Answers2

1

it is not Swift (language) specific, it is Apple specific ... enter image description here enter image description here

even worst if you change 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 apple prints

[:]
[:]
{
    first = john;
    last = doe;
    "test int" = 0;
    "test null" = "<null>";
}

it looks very strange. next code snippet explain 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]

while 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 the slightly modified source code of JSONSerialization from open source distribution (to avoid conflict with apple Foundation I rename the class to _JSONSerialization :-) and change the code such a way it works in my Playground without any warnings and errors and ... it prints the expected results :) enter image description here

Why it works now? The key is

/* 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 the conditional downcasting of all possible values to P works as expected

to be honest, 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]

while linux

[]

and with really big value will crash. this bug goes from (in the open sourced 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 bugs ...)

user3441734
  • 16,722
  • 2
  • 40
  • 59
  • What's the solution ? – Mihir Mehta May 09 '17 at 09:37
  • at every place, should i put as? [String:Any] as? [String:StringOrNumber] ? – Mihir Mehta May 09 '17 at 09:40
  • @mihirmehta yes, or as? [String:AnyObject] as? [String:StringOrNumber], both should work at Apple platform – user3441734 May 09 '17 at 09:55
  • @mihirmehta and fill a bug report to Apple! – user3441734 May 09 '17 at 09:57
  • @mihirmehta change print(jobject) to print(jobject, type(of:jobject)) and see what is the difference between apple and linux implementation. the trouble on Apple goes from bridging and yes, it is definitely a bug on apple platform – user3441734 May 09 '17 at 10:05
  • Thanks ... but my problem will not be solved because i have already usnged [String:StringOrNumber at hundreds of places ... ] even used in inner library module as well. I was testing with literal data ... before replacing it at all places ... – Mihir Mehta May 09 '17 at 10:14
  • Let us [continue this discussion in chat](http://chat.stackoverflow.com/rooms/143767/discussion-between-user3441734-and-mihir-mehta). – user3441734 May 09 '17 at 10:15
  • I have copied the class from https://github.com/apple/swift-corelibs-foundation/blob/master/Foundation/NSJSONSerialization.swift and rename it as _JSONSerialization ... but still i am getting empty dictionary – Mihir Mehta May 10 '17 at 06:06
  • @mihirmehta that is not so easy :-), the source, as it is will not compile on mac, you have to make some changes. how? if properly done, it works, as you can see from the screenshot in my answer. the point was to show, why downcasting to protocol doesn't work with object returning from apple's JSONSerialization.jsonObject. there is nothing bad with your protocol, nothing bad with conditional downcasting, your trouble goes from apple's JSONSerialization. that is all. – user3441734 May 10 '17 at 07:09
  • Yes, Agreed, but do you have JSONSerialization class that compile on mac, as original class uses Private API and vars, I cannot go to double casting way.... Thanks – Mihir Mehta May 10 '17 at 07:13
0

If you want to just check the protocol conformance:

Use is instead of as

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")
}
user1046037
  • 16,755
  • 12
  • 92
  • 138
  • check your solution ... the operator 'is' doesn't work as expected on apple. try let i = 0; i is AnyObject == true; the second will be evaluated as true, even though Int doesn't conform to AnyObject protocol. by the definition: "The is operator returns true if an instance conforms to a protocol and returns false if it does not." as? [String:Any] as? [String:StringOrNumber] doesn't works as expected too :-(. see my answer. – user3441734 May 09 '17 at 17:02
  • That's because implicit conversion to `NSNumber` would happen (free bridging). Please test using other protocols to check conformance – user1046037 May 09 '17 at 17:12
  • Good point, I am not very sure, this is based on my understanding – user1046037 May 09 '17 at 17:25
  • and that is the trouble, by definition Int doesn't conform to AnyObject. try this let arr:[AnyObject] = [Int(0)]; arr.append(i); it will not compile, as expected, with error: value of type 'Int' does not conform to expected element type 'AnyObject' – user3441734 May 09 '17 at 17:27
  • see the last part od my answer, or better try to use data from my example snippet and reconstruct [String:P] dictionary. I am not able to do it on my mac :-) – user3441734 May 09 '17 at 17:31
  • see the final update to my answer, I hope that the explanation is clear. to be able to use conditional downcasting to Swift protocol, returning type of JSONSerialization.jsonObject(..) MUST be one of Swift types! (or type which is bridgeable to Swift type) – user3441734 May 09 '17 at 20:21