2

I have the following JSON object:

[{
    "type": "foo",
    "props": {
        "word": "hello"
    }
}, {
    "type": "bar",
    "props": {
        "number": 42
    }
}]

Depending on the type stored in type, the object in props has different keys. So, I tried with some logic

struct MyObject : Codable {
    struct FooProps { let word: String }
    struct BarProps { var number: Int }
    enum PropTypes { case FooProps, BarProps }

    let type: String
    let props: PropTypes?

    enum CodingKeys : CodingKey {
        case type, props
    }

    init(from decoder: Decoder) throws {
        let values = try decoder.container(keyedBy: CodingKeys.self)
        type = try values.decode(String.self, forKey: .type)
        switch type {
        case "foo":
            props = try values.decode(FooProps.self, forKey: .props)
        case "bar":
            props = try values.decode(BarProps.self, forKey: .props)
        default:
            props = nil
        }
    }
}

but no luck

error: jsontest.playground:10:8: error: type 'MyObject' does not conform to protocol 'Encodable'
struct MyObject : Codable {
       ^

jsontest.playground:16:9: note: cannot automatically synthesize 'Encodable' because 'MyObject.PropTypes?' does not conform to 'Encodable'
    let props: PropTypes?
        ^

error: jsontest.playground:27:39: error: cannot convert value of type 'MyObject.FooProps.Type' to expected argument type 'MyObject.PropTypes?.Type'
            props = try values.decode(FooProps.self, forKey: .props)
                                      ^~~~~~~~

error: jsontest.playground:29:39: error: cannot convert value of type 'MyObject.BarProps.Type' to expected argument type 'MyObject.PropTypes?.Type'
            props = try values.decode(BarProps.self, forKey: .props)
                                      ^~~~~~~~

Then, I thought some class magic would probably do

class PropTypes : Codable { }
class FooProps : PropTypes { var word: String = "Default String" }
class BarProps : PropTypes { var number: Int = -1 }

class MyObject : Codable {
    let type: String
    var props: PropTypes?

    enum CodingKeys : CodingKey {
        case type, props
    }

    ...

but when I do dump the result of the parsing, I only get the default values

▿ 2 elements
  ▿ __lldb_expr_32.MyObject #0
    - type: "foo"
    ▿ props: Optional(__lldb_expr_32.FooProps)
      ▿ some: __lldb_expr_32.FooProps #1
        - super: __lldb_expr_32.PropTypes
        - word: "Default String"
  ▿ __lldb_expr_32.MyObject #2
    - type: "bar"
    ▿ props: Optional(__lldb_expr_32.BarProps)
      ▿ some: __lldb_expr_32.BarProps #3
        - super: __lldb_expr_32.PropTypes
        - number: -1

My question is: what am I missing? Can this be done at all?

EDIT Following Kevin Ballard's suggestion, I get the following errors:

error: jsontest.playground:15:37: error: no 'decode' candidates produce the expected contextual result type 'MyObject.FooProps'
            props = try .foo(values.decode(FooProps.self, forKey: .props))
                                    ^

jsontest.playground:15:37: note: overloads for 'decode' exist with these result types: Bool, Int, Int8, Int16, Int32, Int64, UInt, UInt8, UInt16, UInt32, UInt64, Float, Double, String, T
            props = try .foo(values.decode(FooProps.self, forKey: .props))
                                    ^

error: jsontest.playground:17:37: error: no 'decode' candidates produce the expected contextual result type 'MyObject.BarProps'
            props = try .bar(values.decode(BarProps.self, forKey: .props))
                                    ^

jsontest.playground:17:37: note: overloads for 'decode' exist with these result types: Bool, Int, Int8, Int16, Int32, Int64, UInt, UInt8, UInt16, UInt32, UInt64, Float, Double, String, T
            props = try .bar(values.decode(BarProps.self, forKey: .props))
                                    ^
Morpheu5
  • 2,610
  • 6
  • 39
  • 72

1 Answers1

2

Looking at your original listed errors, there are two distinct issues.

  1. You declared conformance to Codable, but the errors are telling you it cannot automatically synthesize Encodable. Your question isn't about encoding, but decoding, so for this I'd say just conform to Decodable instead of Codable (or implement encoding yourself).
  2. props is of type PropTypes?, where PropTypes is an enum. You're decoding either FooProps or BarProps, and stuffing the result into props. You need to wrap the result in the enum instead. Also your enum is defined wrong, you have cases named FooProps and BarProps, which don't carry values. It should be redefined like { case foo(FooProps), bar(BarPros) } instead.

So together, this would look like

struct MyObject : Decodable {
    struct FooProps : Decodable { let word: String }
    struct BarProps : Decodable { var number: Int }
    enum PropTypes { case foo(FooProps), bar(BarProps) }

    let type: String
    let props: PropTypes?

    enum CodingKeys : CodingKey {
        case type, props
    }

    init(from decoder: Decoder) throws {
        let values = try decoder.container(keyedBy: CodingKeys.self)
        type = try values.decode(String.self, forKey: .type)
        switch type {
        case "foo":
            props = try .foo(values.decode(FooProps.self, forKey: .props))
        case "bar":
            props = try .bar(values.decode(BarProps.self, forKey: .props))
        default:
            props = nil
        }
    }
}
Lily Ballard
  • 182,031
  • 33
  • 381
  • 347
  • 1
    Also your `type` property is now redundant, because you can use the `props` property to determine what the type was. – Lily Ballard Sep 26 '17 at 22:13
  • 1. Thanks, I edited the question to include the error messages I get with your proposed changes. 2. True, but I get it anyway, so might as well use it :) – Morpheu5 Sep 26 '17 at 22:16
  • @Morpheu5 Ah right, `FooProps` and `BarProps` don't conform to `Decodable`. You'll need to update those. You can probably just declare conformance and let it auto-synthesize. I'll update my code snippet to do that. – Lily Ballard Sep 26 '17 at 22:29
  • Regarding `type`, the problem with keeping it is you can then construct values where the `type` and `props` keys disagree about the actual type of the object, e.g. `MyObject(type: "wrong", props: .foo(FooProps(word: "hello")))`. – Lily Ballard Sep 26 '17 at 22:31
  • Indeed, but the actual data I am dealing with comes from a log table that is generated automatically and hardly ever changes – and when it does, it's versioned. – Morpheu5 Sep 26 '17 at 22:33
  • Regarding the `Decodable` bit: that's it. Solved. Thanks a million :D You have no idea how many hours I wasted on this, today. – Morpheu5 Sep 26 '17 at 22:34