2

I'm using a protocol to create several structs which I use to decode using JSONDecoder. Here's a code sample of what I'm trying to achieve.

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
}

Here are the seperate JSON payloads of dog and cat:

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

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

So when I decode the JSON, I'm not sure what JSON I'll have, dog or cat. I tried doing this:

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

But end up with this error:

In argument type 'Animal.Protocol', 'Animal' does not conform to expected type 'Decodable'

Any ideas as to the best approach to parse either dog or cat returning an instance of Animal?

Thanks

user9041624
  • 246
  • 4
  • 14
  • Dog and cat need Codable protocol too – Scriptable May 01 '18 at 07:28
  • Given the JSON above, this isn't solvable. Nothing in the JSON indicates whether this is a Dog, Cat, or any of infinite other types that might conform to Animal. If you know "this is a Dog or a Cat" then the above is absolutely solvable (provided that you have a test to determine Dogs from Cats in the JSON, such as "Dogs have a type"), but not "it's an Animal." It might be an Animal your module doesn't know about (perhaps one defined in another module). This is also solvable with type-erasure, but you'll get an AnyAnimal, not a Dog or Cat. – Rob Napier May 01 '18 at 16:29

3 Answers3

0

A better approach would be to use a class instead of a protocol and use classes instead of structs. Your Dog and Cat classes will be subclasses of 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)
Nader
  • 1,120
  • 1
  • 9
  • 22
0

You're not going to be able to use this:

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

To decode a Dog or a Cat. It's always going to be an Animal.

If you want to decode both those JSON objects to Animal, then define Animal like this:

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

Of course, you'll lose the distinctive elements that make them a Dog (type) or Cat (color).

Mike Taverne
  • 9,156
  • 2
  • 42
  • 58
0

Your opening a somewhat ugly can of worms here. I understand what you try to do, but unfortunately it fails in a number of ways. You can get somewhat close to what you want with the following Playground:

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

However you will notice that there are some changes that do have a reason. As a matter of fact you cannot implement the Decodable method init(from: Decoder) throws since it is supposed to chain to the init method which ... does not really work out for a protocol. I chose instead to implement your favourite dispatcher in the Animal.make method, but this ended up as a half baked solution as well. Since protocols are metatypes (probably for a good reason as well) you are not able to call their static methods on the metatype and have to use a concrete one. As the line Dog.make(fromJSON: catData) shows this looks weird to say the least. It would be better to bake this into a top level function such as

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

but still this looks unsatisfactory in another way since it pollutes the global namespace. Probably still the best we can do with the means available.

Given the ugliness of the dispatcher it seems like a bad idea to have JSON with no direct indication of the type since it makes parsing really hard. However, I do not see a nice way to communicate a subtype in JSON in a way that really makes it easy to parse. Have not done any research on this, but it might be your next try.

Patru
  • 4,481
  • 2
  • 32
  • 42