Как мне проанализировать объект JSON, который имеет подобъекты, зависящие от типа, в Swift?

У меня есть следующий объект JSON:

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

В зависимости от типа, хранящегося в type, объект в props имеет разные ключи. Итак, я попытался с некоторой логикой

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
        }
    }
}

но не повезло

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)
                                      ^~~~~~~~

Тогда я подумал, что немного классовой магии, вероятно, сработает.

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
    }

    ...

но когда я делаю dump результат синтаксического анализа, я получаю только значения по умолчанию

▿ 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

Мой вопрос: что мне не хватает? Это вообще можно сделать?

EDIT Следуя предложению Кевина Балларда, я получаю следующие ошибки:

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))
                                    ^

person Morpheu5    schedule 26.09.2017    source источник


Ответы (1)


Глядя на ваши исходные перечисленные ошибки, есть две разные проблемы.

  1. Вы заявили о соответствии Codable, но ошибки говорят вам, что он не может автоматически синтезировать Encodable. Ваш вопрос касается не кодирования, а декодирования, поэтому для этого я бы сказал, просто соблюдайте Decodable вместо Codable (или реализуйте кодирование самостоятельно).
  2. props имеет тип PropTypes?, где PropTypes — перечисление. Вы декодируете FooProps или BarProps и вставляете результат в props. Вместо этого вам нужно обернуть результат в перечисление. Также ваше перечисление определено неправильно, у вас есть случаи с именами FooProps и BarProps, которые не несут значений. Вместо этого его следует переопределить как { case foo(FooProps), bar(BarPros) }.

Итак, вместе это будет выглядеть как

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
        }
    }
}
person Lily Ballard    schedule 26.09.2017
comment
Кроме того, ваше свойство type теперь избыточно, потому что вы можете использовать свойство props, чтобы определить тип. - person Lily Ballard; 27.09.2017
comment
1. Спасибо, я отредактировал вопрос, включив в него сообщения об ошибках, которые я получаю с предложенными вами изменениями. 2. Верно, но я все равно понял, так что можно и использовать :) - person Morpheu5; 27.09.2017
comment
@ Morpheu5 А, да, FooProps и BarProps не соответствуют Decodable. Вам нужно будет обновить их. Вероятно, вы можете просто объявить о соответствии и позволить ему автоматически синтезироваться. Я обновлю свой фрагмент кода, чтобы сделать это. - person Lily Ballard; 27.09.2017
comment
Что касается type, проблема с его сохранением заключается в том, что вы можете создавать значения, в которых ключи type и props не совпадают с фактическим типом объекта, например. MyObject(type: "wrong", props: .foo(FooProps(word: "hello"))). - person Lily Ballard; 27.09.2017
comment
Действительно, но фактические данные, с которыми я имею дело, поступают из таблицы журнала, которая генерируется автоматически и практически никогда не изменяется, а если и изменяется, то она версионируется. - person Morpheu5; 27.09.2017
comment
Что касается бита Decodable: вот и все. Решено. Спасибо миллион :D Вы не представляете, сколько часов я потратил на это сегодня. - person Morpheu5; 27.09.2017