0

I have the following data structures in place

struct Symbol : Codable {
    let id:Int
    let type:String
    let properties:[String:String]?
}

struct Event : Codable {
    let event:String
    let timestamp:Int
    let symbol:Symbol?
}

struct LogRow : Codable {
    let id:Int
    let user_id:String
    let question_id:String
    let actions:[Event]
    let timestamp:Double
}

and the following JSON array

[
  {
    "id": 26535754,
    "user_id": "qhv1i39wsmbkzhjiffk1rrsg",
    "question_id": "\"trapdoor|186752c1-948e-4c15-b3df-7d39a99fe9d6\"",
    "actions": "[{\"event\": \"OPEN\", \"timestamp\": 1499802241640}, {\"event\": \"DRAG_START\", \"symbol\": {\"id\": 15, \"type\": \"Fn\", \"properties\": {\"name\": \"cos\", \"allowSubscript\": true, \"innerSuperscript\": true}}, \"timestamp\": 1499802243567}, {\"event\": \"UNDOCK_SYMBOL\", \"parent\": {\"id\": 14, \"type\": \"Fraction\"}, \"symbol\": {\"id\": 15, \"type\": \"Fn\", \"properties\": {\"name\": \"cos\", \"allowSubscript\": true, \"innerSuperscript\": true}}, \"timestamp\": 1499802243567, \"dockingPoint\": \"right\"}, {\"event\": \"DOCK_SYMBOL\", \"parent\": {\"id\": 13, \"type\": \"Fraction\"}, \"symbol\": {\"id\": 15, \"type\": \"Fn\", \"properties\": {\"name\": \"cos\", \"allowSubscript\": true, \"innerSuperscript\": true}}, \"timestamp\": 1499802243699, \"dockingPoint\": \"denominator\"}, {\"event\": \"DRAG_START\", \"symbol\": {\"id\": 16, \"type\": \"Num\", \"properties\": {\"significand\": \"60\"}}, \"timestamp\": 1499802244570}, {\"event\": \"UNDOCK_SYMBOL\", \"parent\": {\"id\": 15, \"type\": \"Fn\", \"properties\": {\"name\": \"cos\", \"allowSubscript\": true, \"innerSuperscript\": true}}, \"symbol\": {\"id\": 16, \"type\": \"Num\", \"properties\": {\"significand\": \"60\"}}, \"timestamp\": 1499802244570, \"dockingPoint\": \"argument\"}, {\"event\": \"DROP_SYMBOL\", \"symbol\": {\"id\": 16, \"type\": \"Num\", \"properties\": {\"significand\": \"60\"}}, \"timestamp\": 1499802245281}, {\"event\": \"DRAG_START\", \"symbol\": {\"id\": 13, \"type\": \"Fraction\"}, \"timestamp\": 1499802245845}, {\"event\": \"TRASH_SYMBOL\", \"symbol\": {\"id\": 13, \"type\": \"Fraction\"}, \"timestamp\": 1499802246826}, {\"event\": \"DRAG_START\", \"symbol\": {\"id\": 16, \"type\": \"Num\", \"properties\": {\"significand\": \"60\"}}, \"timestamp\": 1499802247468}, {\"event\": \"TRASH_SYMBOL\", \"symbol\": {\"id\": 16, \"type\": \"Num\", \"properties\": {\"significand\": \"60\"}}, \"timestamp\": 1499802248360}, {\"event\": \"DRAG_START\", \"symbol\": {\"id\": 19, \"type\": \"Num\", \"properties\": {\"significand\": \"2\"}}, \"timestamp\": 1499802249161}, {\"event\": \"UNDOCK_SYMBOL\", \"parent\": {\"id\": 14, \"type\": \"Fraction\"}, \"symbol\": {\"id\": 19, \"type\": \"Num\", \"properties\": {\"significand\": \"2\"}}, \"timestamp\": 1499802249161, \"dockingPoint\": \"denominator\"}, {\"event\": \"DOCK_SYMBOL\", \"parent\": {\"id\": 14, \"type\": \"Fraction\"}, \"symbol\": {\"id\": 19, \"type\": \"Num\", \"properties\": {\"significand\": \"2\"}}, \"timestamp\": 1499802249289, \"dockingPoint\": \"denominator\"}, {\"event\": \"DRAG_START\", \"symbol\": {\"id\": 19, \"type\": \"Num\", \"properties\": {\"significand\": \"2\"}}, \"timestamp\": 1499802249797}, {\"event\": \"UNDOCK_SYMBOL\", \"parent\": {\"id\": 14, \"type\": \"Fraction\"}, \"symbol\": {\"id\": 19, \"type\": \"Num\", \"properties\": {\"significand\": \"2\"}}, \"timestamp\": 1499802249797, \"dockingPoint\": \"denominator\"}, {\"event\": \"DOCK_SYMBOL\", \"parent\": {\"id\": 14, \"type\": \"Fraction\"}, \"symbol\": {\"id\": 19, \"type\": \"Num\", \"properties\": {\"significand\": \"2\"}}, \"timestamp\": 1499802249906, \"dockingPoint\": \"denominator\"}, {\"event\": \"DRAG_POTENTIAL_SYMBOL\", \"symbol\": {\"id\": 20, \"type\": \"Num\", \"properties\": {\"significand\": \"4\"}}, \"timestamp\": 1499802251451}, {\"event\": \"DROP_POTENTIAL_SYMBOL\", \"symbol\": {\"id\": 20, \"type\": \"Num\", \"properties\": {\"significand\": \"4\"}}, \"timestamp\": 1499802254229}, {\"event\": \"DRAG_START\", \"symbol\": {\"id\": 19, \"type\": \"Num\", \"properties\": {\"significand\": \"2\"}}, \"timestamp\": 1499802255231}, {\"event\": \"UNDOCK_SYMBOL\", \"parent\": {\"id\": 14, \"type\": \"Fraction\"}, \"symbol\": {\"id\": 19, \"type\": \"Num\", \"properties\": {\"significand\": \"2\"}}, \"timestamp\": 1499802255231, \"dockingPoint\": \"denominator\"}, {\"event\": \"TRASH_SYMBOL\", \"symbol\": {\"id\": 19, \"type\": \"Num\", \"properties\": {\"significand\": \"2\"}}, \"timestamp\": 1499802256593}, {\"event\": \"DRAG_START\", \"symbol\": {\"id\": 20, \"type\": \"Num\", \"properties\": {\"significand\": \"4\"}}, \"timestamp\": 1499802257376}, {\"event\": \"DOCK_SYMBOL\", \"parent\": {\"id\": 14, \"type\": \"Fraction\"}, \"symbol\": {\"id\": 20, \"type\": \"Num\", \"properties\": {\"significand\": \"4\"}}, \"timestamp\": 1499802258062, \"dockingPoint\": \"denominator\"}, {\"event\": \"CLOSE\", \"timestamp\": 1499802259093}]",
    "timestamp": 1.499802259277E9
  },
  {
    "id": 26535718,
    "user_id": "qhv1i39wsmbkzhjiffk1rrsg",
    "question_id": "\"trapdoor|186752c1-948e-4c15-b3df-7d39a99fe9d6\"",
    "actions": "[{\"event\": \"OPEN\", \"timestamp\": 1499802175061}, {\"event\": \"DRAG_POTENTIAL_SYMBOL\", \"symbol\": {\"id\": 10, \"type\": \"Fraction\"}, \"timestamp\": 1499802178936}, {\"event\": \"DOCK_POTENTIAL_SYMBOL\", \"parent\": {\"id\": 7, \"type\": \"Symbol\", \"properties\": {\"letter\": \"g\"}}, \"symbol\": {\"id\": 10, \"type\": \"Fraction\"}, \"timestamp\": 1499802183785, \"dockingPoint\": \"subscript\"}, {\"event\": \"DRAG_START\", \"symbol\": {\"id\": 10, \"type\": \"Fraction\"}, \"timestamp\": 1499802184864}, {\"event\": \"UNDOCK_SYMBOL\", \"parent\": {\"id\": 7, \"type\": \"Symbol\", \"properties\": {\"letter\": \"g\"}}, \"symbol\": {\"id\": 10, \"type\": \"Fraction\"}, \"timestamp\": 1499802184865, \"dockingPoint\": \"subscript\"}, {\"event\": \"DOCK_SYMBOL\", \"parent\": {\"id\": 6, \"type\": \"Symbol\", \"properties\": {\"letter\": \"m\"}}, \"symbol\": {\"id\": 10, \"type\": \"Fraction\"}, \"timestamp\": 1499802185857, \"dockingPoint\": \"subscript\"}, {\"event\": \"DRAG_START\", \"symbol\": {\"id\": 10, \"type\": \"Fraction\"}, \"timestamp\": 1499802186710}, {\"event\": \"UNDOCK_SYMBOL\", \"parent\": {\"id\": 6, \"type\": \"Symbol\", \"properties\": {\"letter\": \"m\"}}, \"symbol\": {\"id\": 10, \"type\": \"Fraction\"}, \"timestamp\": 1499802186710, \"dockingPoint\": \"subscript\"}, {\"event\": \"DROP_SYMBOL\", \"symbol\": {\"id\": 10, \"type\": \"Fraction\"}, \"timestamp\": 1499802188430}, {\"event\": \"DRAG_POTENTIAL_SYMBOL\", \"symbol\": {\"id\": 11, \"type\": \"Fraction\"}, \"timestamp\": 1499802194665}, {\"event\": \"DROP_POTENTIAL_SYMBOL\", \"symbol\": {\"id\": 11, \"type\": \"Fraction\"}, \"timestamp\": 1499802195427}, {\"event\": \"DRAG_START\", \"symbol\": {\"id\": 6, \"type\": \"Symbol\", \"properties\": {\"letter\": \"m\"}}, \"timestamp\": 1499802196167}, {\"event\": \"DOCK_SYMBOL\", \"parent\": {\"id\": 11, \"type\": \"Fraction\"}, \"symbol\": {\"id\": 6, \"type\": \"Symbol\", \"properties\": {\"letter\": \"m\"}}, \"timestamp\": 1499802197167, \"dockingPoint\": \"numerator\"}, {\"event\": \"DRAG_START\", \"symbol\": {\"id\": 8, \"type\": \"Fn\", \"properties\": {\"name\": \"cos\", \"allowSubscript\": true, \"innerSuperscript\": true}}, \"timestamp\": 1499802198247}, {\"event\": \"UNDOCK_SYMBOL\", \"parent\": {\"id\": 7, \"type\": \"Symbol\", \"properties\": {\"letter\": \"g\"}}, \"symbol\": {\"id\": 8, \"type\": \"Fn\", \"properties\": {\"name\": \"cos\", \"allowSubscript\": true, \"innerSuperscript\": true}}, \"timestamp\": 1499802198247, \"dockingPoint\": \"right\"}, {\"event\": \"DOCK_SYMBOL\", \"parent\": {\"id\": 11, \"type\": \"Fraction\"}, \"symbol\": {\"id\": 8, \"type\": \"Fn\", \"properties\": {\"name\": \"cos\", \"allowSubscript\": true, \"innerSuperscript\": true}}, \"timestamp\": 1499802199971, \"dockingPoint\": \"right\"}, {\"event\": \"DRAG_START\", \"symbol\": {\"id\": 11, \"type\": \"Fraction\"}, \"timestamp\": 1499802201551}, {\"event\": \"DROP_SYMBOL\", \"symbol\": {\"id\": 11, \"type\": \"Fraction\"}, \"timestamp\": 1499802202638}, {\"event\": \"DRAG_POTENTIAL_SYMBOL\", \"symbol\": {\"id\": 12, \"type\": \"Num\", \"properties\": {\"significand\": \"2\"}}, \"timestamp\": 1499802209025}, {\"event\": \"DOCK_POTENTIAL_SYMBOL\", \"parent\": {\"id\": 11, \"type\": \"Fraction\"}, \"symbol\": {\"id\": 12, \"type\": \"Num\", \"properties\": {\"significand\": \"2\"}}, \"timestamp\": 1499802210771, \"dockingPoint\": \"denominator\"}, {\"event\": \"CLOSE\", \"timestamp\": 1499802212398}]",
    "timestamp": 1.49980221259E9
  }
]

The JSON data comes from a database, exported via DataGrip's JSON export. The database field is of type JSON Blob (it's Postgres, so it's OK). The query is something like:

SELECT
  ...
  event_details->>'actions' AS actions,
  ...
FROM yadda_yadda...;

in case it's useful (yes, I also tried the single arrow version, no difference).

My question is how do I make Swift parse the actions string as a JSON object into an [Event]? Alternatively, is there a way of making DataGrip (or postgres) export that field as an object like its container rather than as a serialised object?

EDIT I changed LogRow like this

struct LogRow : Codable {
    let id:Int
    let user_id:String
    let question_id:String
    let actions:[Event]
    let timestamp:Double

    enum CodingKeys: String, CodingKey {
        case id
        case user_id
        case question_id
        case actions
        case timestamp
    }

    init(from decoder: Decoder) throws {
        let values = try decoder.container(keyedBy: CodingKeys.self)
        id = try values.decode(Int.self, forKey: .id)
        user_id = try values.decode(String.self, forKey: .user_id)
        question_id = try values.decode(String.self, forKey: .question_id)
        timestamp = try values.decode(Double.self, forKey: .timestamp)

        let actions_string = try values.decode(String.self, forKey: .actions)
        let actions_data = actions_string.data(using: .utf8)!
        actions = try JSONDecoder().decode([Event].self, from: actions_data)
    }
}

and now I'm getting a similar error to before, but apparently on the Event, not on the LogRow anymore.

fatal error: Error raised at top level:
Swift.DecodingError.typeMismatch(Swift.String,
Swift.DecodingError.Context(codingPath: [Foundation.(_JSONKey in
_12768CA107A31EF2DCE034FD75B541C9)(stringValue: "Index 1", intValue:
Optional(1)), tree_builder.Event.(CodingKeys in
_D5964B2C6A943A986EE24818C2C63D9B).symbol, tree_builder.Symbol.
(CodingKeys in _D5964B2C6A943A986EE24818C2C63D9B).properties,
Swift._DictionaryCodingKey(stringValue: "allowSubscript", intValue:
nil)], debugDescription: "Expected to decode String but found a number
instead.", underlyingError: nil)): file
/Library/Caches/com.apple.xbs/Sources/swiftlang/swiftlang-
900.0.65/src/swift/stdlib/public/core/ErrorType.swift, line 187
Morpheu5
  • 2,610
  • 6
  • 39
  • 72
  • First you should get rid of all this implicitly unwrapped optionals. this answer shows you how to accomplish it https://stackoverflow.com/a/43121890/2303865 – Leo Dabus Sep 26 '17 at 12:41
  • I normally would, but in this case I am absolutely certain I can implicitly unwrap my optionals, as I completely control the environment, including the data set. If I were to release this in the wild, I would definitely get rid of that. – Morpheu5 Sep 26 '17 at 12:46
  • 2
    The point it is not if you can or can't. You don't need it. – Leo Dabus Sep 26 '17 at 12:52
  • Huh… is this something that changed with Swift 4? I started working on this code a few days ago on 3.1, and I was pretty sure it was complaining about optionals not being unwrapped. – Morpheu5 Sep 26 '17 at 12:54
  • I think it changed with Swift 3.x https://github.com/apple/swift-evolution/blob/master/proposals/0054-abolish-iuo.md – Leo Dabus Sep 26 '17 at 12:56
  • 1
    Makes sense. Thanks for pointing it out, I wouldn't have checked for a while :) – Morpheu5 Sep 26 '17 at 13:00
  • https://developer.apple.com/documentation/foundation/archives_and_serialization/encoding_and_decoding_custom_types – Leo Dabus Sep 26 '17 at 13:54
  • I'm trying just that but having further troubles. I'll update the question in a moment. – Morpheu5 Sep 26 '17 at 13:54
  • https://stackoverflow.com/a/44396824/2303865 – Leo Dabus Sep 26 '17 at 13:58
  • Built-in types such as Array , Dictionary , and Optional also conform to Codable whenever they contain codable types. You can add an array of Coordinate instances to Landmark, and the entire structure will still satisfy Codable. – Leo Dabus Sep 26 '17 at 14:01
  • But I need to output a `[Event]`, and in fact I get an error when I change that. – Morpheu5 Sep 26 '17 at 14:04
  • I can't check your code right now. You should'n need to implement the decoder – Leo Dabus Sep 26 '17 at 14:05
  • I shouldn't if I were OK with `actions` being a string which represents a JSON object. Apparently I do need to implement the decoder if I want that string to be parsed into a proper Swift object. I just managed to do this by not deserialising the `symbol` property in `Event`, which is kind of a bummer. I'll look into that and update my question again. Thanks for your help so far. – Morpheu5 Sep 26 '17 at 14:11
  • your Symbol properties dictionary type should be `[String:Any]` but I am not sure if you can use this dictionary type with Encodable – Leo Dabus Sep 26 '17 at 15:57
  • cannot automatically synthesize 'Decodable' because '[String : Any]?' does not conform to 'Decodable' – Leo Dabus Sep 26 '17 at 16:05
  • I think you would need another struct for Properties – Leo Dabus Sep 26 '17 at 16:06
  • Exactly, but the properties can vary depending on the type of the object. I could probably have a massive `struct Properties : Codable` with everything I might ever need in it (not sure it would work anyway) but it'd be much nicer if I could just have different classes (because structs can't inherit non-protocol types, apparently) with their own different properties. That may be messy in its own right, and this whole exercise is starting to become a huge overkill for what I actually needed to do in the first place. – Morpheu5 Sep 26 '17 at 16:16

2 Answers2

1

You can use optional chaining, this is the solution given by Apple : https://developer.apple.com/swift/blog/?id=37

There's also library that does it for you, like https://github.com/Hearst-DD/ObjectMapper, or https://github.com/tristanhimmelman/AlamofireObjectMapper

This is a basic example on how to do it, but you should refactor and take a look at the links I gave :

guard let event = jsonObject["event"] as? String, let timestamp = jsonObject["timestamp"] as? Int else {return}
myEvent.event = event
myEvent.timestamp = timestamp 
if let symbol = jsonObject["symbol"] as? [String:Any] {
    guard let symbolId = symbol["id"] as? Int, let type = symbol["type"] else {return}
    let mySymbol = symbol()
    mySymbol.id = id
    mySymbol.type = type
}

EDIT

If you are using swift 4 and Codable protocol it should be really easy, you just have to make your nested objects as Codable too ! Then just :

try decoder.decode(yourType.self, from: yourJsonString)

If you use CodingKeys correctly it should do it.

Damien
  • 3,322
  • 3
  • 19
  • 29
  • Uuuh, my question was not how to decode the json string into an object. That works if I feed a test string to a decoder and ask it to spit out an `Event` object. My question was: given that the serialised `LogRow` contains a string containing a serialised representation of an array of `Event`s in its `actions` key, how do I make the decoder deserialise that string and turn it into an array of `Event`s? – Morpheu5 Sep 26 '17 at 13:33
  • https://developer.apple.com/documentation/foundation/archives_and_serialization/encoding_and_decoding_custom_types – Leo Dabus Sep 26 '17 at 13:54
0

The problem was the declaration of properties, as the JSON object does not encode a [String:String] dictionary, since some of the values may not be Strings. So, this is the "answer" to this question, but that does not solve the nesting problem, so I created a new question: How do I parse a JSON object that has type-dependent sub-objects, in Swift?

Morpheu5
  • 2,610
  • 6
  • 39
  • 72