Repository: segmentio/analytics-swift (swift)

Issue:
JSON encoding: lots of Codable-conforming types that crash in JSON encoding
**Describe the bug**
There are many swift data types that crash in the library's JSON handling that (I believe) shouldn't. And since the type that the JSON object accepts in its initializer is `[String: Any]`, the compiler can't catch any of these issues before runtime. This makes working with the library difficult (since you have to know which types are accepted and manually convert everything else to those types) and fragile (since you need to manually test each tracking call to ensure it doesn't crash the JSON logic).

**To Reproduce**
Steps to reproduce the behavior:
1. Do `segment.track(name: "Some Event Name", properties: ["key": value])` where the value is any of the following types (there are probably more, but these are the ones I've run into so far):
    - UUID
    - enum that conforms to Codable (e.g. `enum MyEnum: String, Codable { case a, b, c }`)
2. See the following error:
```
Segment/Utils.swift:54: Fatal error: A critical error occurred: jsonUnableToDeserialize(Segment.JSON.JSONError.nonJSONType(type: ...))
```

**Expected behavior**
Ideally, types that conform to `Encodable` and that can be properly serialized using a regular `JSONEncoder` should not crash in the library's handling of JSON

**Platform (please complete the following information):**
 - Library Version in use: 1.5.3
 - Platform being tested: iOS

Pull request description:
- Allow codable compliant types in JSON's init(any).
- Added test
- Fixes #301

Patch:
diff --git a/Sources/Segment/Utilities/JSON.swift b/Sources/Segment/Utilities/JSON.swift
index 63de64e7..fd8a79a0 100644
--- a/Sources/Segment/Utilities/JSON.swift
+++ b/Sources/Segment/Utilities/JSON.swift
@@ -76,7 +76,7 @@ public enum JSON: Equatable {
     public init<T: Codable>(with value: T) throws {
         let encoder = JSONSafeEncoder.default
         let json = try encoder.encode(value)
-        let output = try JSONSerialization.jsonObject(with: json)
+        let output = try JSONSerialization.jsonObject(with: json, options: .fragmentsAllowed)
         try self.init(output)
     }
     
@@ -113,6 +113,8 @@ public enum JSON: Equatable {
             self = .object(try object.mapValues(JSON.init))
         case let json as JSON:
             self = json
+        case let codable as Codable:
+            self = try Self.init(with: codable)
         // we don't work with whatever is being supplied
         default:
             throw JSONError.nonJSONType(type: "\(value.self)")


Test patch:
diff --git a/Tests/Segment-Tests/JSON_Tests.swift b/Tests/Segment-Tests/JSON_Tests.swift
index 829b73de..fd744771 100644
--- a/Tests/Segment-Tests/JSON_Tests.swift
+++ b/Tests/Segment-Tests/JSON_Tests.swift
@@ -411,4 +411,59 @@ class JSONTests: XCTestCase {
             XCTFail()
         }
     }
+    
+    func testJSONCodableDict() throws {
+        enum StringEnum: String, Codable {
+            case test1
+            case test2
+            case test3
+        }
+        
+        enum IntEnum: Int, Codable {
+            case test1
+            case test2
+            case test3
+        }
+        
+        struct SubStruct: Codable {
+            var x: Int = 23
+        }
+        
+        struct CodableStruct: Codable {
+            var a: Int = 47
+            var b: String = "hello"
+            var c: SubStruct = SubStruct()
+        }
+        
+        let dict: [String: Any] = [
+            "uuid": UUID(),
+            "strEnum": StringEnum.test2,
+            "intEnum": IntEnum.test2,
+            "struct": CodableStruct()
+        ]
+        
+        do {
+            let json = try JSON(dict)
+            print(json.prettyPrint())
+            
+            let strEnum: String? = json[keyPath: "strEnum"]
+            XCTAssertEqual(strEnum, "test2")
+            
+            let intEnum: Int? = json[keyPath: "intEnum"]
+            XCTAssertEqual(intEnum, 1)
+            
+            let b: String? = json[keyPath: "struct.b"]
+            XCTAssertEqual(b, "hello")
+            
+            let x: Int? = json[keyPath: "struct.c.x"]
+            XCTAssertEqual(x, 23)
+            
+            let uuid: String? = json[keyPath: "uuid"]
+            XCTAssertEqual(uuid!.count, 36)
+            
+        } catch {
+            print(error)
+            XCTFail()
+        }
+    }
 }