JSONDecoder с использованием протокола

Я использую протокол для создания нескольких структур, которые я использую для декодирования с помощью JSONDecoder. Вот пример кода того, чего я пытаюсь достичь.

protocol Animal: Codable
{
   var name: String { get }
   var age: Int { get }
}

struct Dog: Animal
{
   let name: String
   let age: Int
   let type: String
}

struct Cat: Animal
{
   let name: String
   let age: Int
   let color: String
}

Вот отдельные полезные данные JSON для собак и кошек:

{
    "name": "fleabag",
    "age": 3,
    "type": "big"
}

{
    "name": "felix",
    "age": 2,
    "color": "black"
}

Поэтому, когда я декодирую JSON, я не уверен, какой JSON у меня будет, собачий или кошачий. Я пытался сделать это:

let data = Data(contentsOf: url)
let value = JSONDecoder().decode(Animal.self, from: data)

Но в итоге с этой ошибкой:

В типе аргумента «Animal.Protocol» «Животное» не соответствует ожидаемому типу «Декодируемый».

Любые идеи относительно наилучшего подхода к разбору собаки или кошки, возвращающих экземпляр Animal?

Спасибо


person user9041624    schedule 01.05.2018    source источник
comment
Собаке и кошке тоже нужен протокол Codable   -  person Scriptable    schedule 01.05.2018
comment
Учитывая приведенный выше JSON, это неразрешимо. Ничто в JSON не указывает, является ли это собакой, кошкой или любым из множества других типов, которые могут соответствовать животному. Если вы знаете, что это Собака или Кошка, то вышесказанное абсолютно решаемо (при условии, что у вас есть тест для определения Собаки от Кошки в JSON, например, у Собаки есть тип), но не это Животное. Это может быть животное, о котором ваш модуль не знает (возможно, оно определено в другом модуле). Это также решается стиранием типа, но вы получите AnyAnimal, а не Dog или Cat.   -  person Rob Napier    schedule 01.05.2018


Ответы (3)


Вы не сможете использовать это:

let animal = try? JSONDecoder().decode(Animal.self, from: data)

Расшифровать Собаку или Кошку. Это всегда будет Животное.

Если вы хотите декодировать оба этих объекта JSON в Animal, определите Animal следующим образом:

struct Animal: Codable {
    var name: String
    var age: Int
}

Конечно, вы потеряете отличительные элементы, которые делают их собакой (type) или кошкой (color).

person Mike Taverne    schedule 01.05.2018

Вы открываете довольно уродливую банку с червями. Я понимаю, что вы пытаетесь сделать, но, к сожалению, во многих отношениях это не удается. Вы можете приблизиться к тому, что хотите, со следующей игровой площадкой:

import Cocoa

let dogData = """
{
    "name": "fleabag",
    "age": 3,
    "type": "big"
}
""".data(using: .utf8)!

let catData = """
{
    "name": "felix",
    "age": 2,
    "color": "black"
}
""".data(using: .utf8)!

protocol Animal: Codable
{
    var name: String { get }
    var age: Int { get }
}

struct Dog: Animal
{
    let name: String
    let age: Int
    let type: String
}

struct Cat: Animal
{
    let name: String
    let age: Int
    let color: String
}

do {
    let decoder = JSONDecoder()
    let dog = try decoder.decode(Dog.self, from: dogData)
    print(dog)
    let cat = try decoder.decode(Cat.self, from: catData)
    print(cat)
}

extension Animal {
    static func make(fromJSON data: Data) -> Animal? {
        let decoder = JSONDecoder()
        do {
            let dog = try decoder.decode(Dog.self, from: data)
            return dog
        } catch {
            do {
                let cat = try decoder.decode(Cat.self, from: data)
                return cat
            } catch {
                return nil
            }
        }
    }
}

if let animal = Dog.make(fromJSON: dogData) {
    print(animal)
}
if let animal2 = Dog.make(fromJSON: catData) {
    print(animal2)
}

Однако вы заметите, что есть некоторые изменения, у которых есть причина. На самом деле вы не можете реализовать метод Decodable init(from: Decoder) throws, так как предполагается, что он chain соответствует методу init, который... на самом деле не работает для протокола. Вместо этого я решил реализовать ваш любимый диспетчер в методе Animal.make, но это также оказалось полусырым решением. Поскольку protocols являются метатипами (вероятно, и по уважительной причине), вы не можете вызывать их статические методы для метатипа и должны использовать конкретный. Как показывает строка Dog.make(fromJSON: catData), это выглядит, мягко говоря, странно. Было бы лучше запечь это в функцию верхнего уровня, такую ​​как

func parseAnimal(from data:Data) {
    ...
}

но все же это выглядит неудовлетворительно с другой стороны, поскольку загрязняет глобальное пространство имен. Вероятно, это лучшее, что мы можем сделать с имеющимися средствами.

Учитывая уродство диспетчера, кажется плохой идеей иметь JSON без прямого указания типа, поскольку это очень усложняет синтаксический анализ. Однако я не вижу хорошего способа передать подтип в JSON таким образом, чтобы его действительно было легко анализировать. Не проводил никаких исследований по этому поводу, но это может быть ваша следующая попытка.

person Patru    schedule 01.05.2018

Лучшим подходом было бы использование класса вместо протокола и использование классов вместо структур. Ваши классы Dog и Cat будут подклассами Animal

class Animal: Codable {
    let name: String
    let age: Int

    private enum CodingKeys: String, CodingKey {
        case name
        case age
    }
}

class Dog: Animal {
    let type: String

    private enum CodingKeys: String, CodingKey {
        case type
    }

    required init(from decoder: Decoder) throws {
        let container = try decoder.container(keyedBy: CodingKeys.self)
        self.type = try container.decode(String.self, forKey: .type)
        try super.init(from: decoder)
    }
}

class Cat: Animal {
    let color: String

    private enum CodingKeys: String, CodingKey {
        case color
    }

    required init(from decoder: Decoder) throws {
        let container = try decoder.container(keyedBy: CodingKeys.self)
        self.color = try container.decode(String.self, forKey: .color)
        try super.init(from: decoder)
    }
}

let data = Data(contentsOf: url)
let animal = JSONDecoder().decode(Animal.self, from: data)
person Nader Besada    schedule 01.05.2018
comment
if animal is Dog никогда не будет правдой - person Mike Taverne; 01.05.2018
comment
В соответствии с вашей class декларацией вы можете проверить if dog is Animal, чтобы if childClass is parentClass. - person TheTiger; 01.05.2018
comment
Я понимаю if dog is Animal, но не понимаю if childClass is parentClass - person user9041624; 01.05.2018
comment
@user9041624 user9041624 Вы можете только проверить, что childClass является типом parentClass, а не parentClass является типом childClass. Здесь Dog является дочерним элементом Animal, а не Animal является дочерним элементом Dog. - person TheTiger; 01.05.2018
comment
спасибо за пример. Однако то, что говорит @Mike Taverne, верно. - person user9041624; 01.05.2018