BLACK FRIDAY: Save 50% on all my Swift books and bundles! >>

SOLVED: Can you make a non-concrete type conform to codable?

Forums > Swift

Hi All,

I'm looking at Composition over Inheritance but am running into problems trying to decode, as it appears to need concrete types (even if the the protocols themselves conform)?

A quick outline...very loose, I know, but should suggest what I'm after

enum EnumSubItemType {
  case subItem01, subItem01
}

protocol SubItemProtocol: Codable {
}

struct SubItemModel01: SubItemProtocol {
// Necessary bits (inits, encoders, decoders, etc)
}

struct SubItemModel02: SubItemProtocol {
// As Above
}

class MainItem: Codable {
var name: String
var subItemType: EnumSubItemType
var subItem: SubItemProtocol

  enum CodingKeys: String, CodingKey {
    case name
    case subItemType
    case subItem
  }

  required init(from decoder: Decoder) throws {

    let values = try decoder.container(keyedBy: CodingKeys.self)

    self.name = try values.decode(String.self, forKey: .name)
    self.subItemType = try values.decode(String.self, forKey: .subItemType)

      let subItemContainer = try values.superDecoder(forKey: .subItem)
      let subItemDecoder = try values.superDecoder(forKey: .subItem)

    switch self.objectType {

    case .subItem01:
        self.subItem = try subItemDecoder.decode(SubItem01.self, forKey: .subItem)
        // Other cases

    default:
      self.subItem = nil

    }

  }

  func encode(to encoder: Encoder) throws {
    var container = encoder.container(keyedBy: CodingKeys.self)
    try container.encode(name, forKey: .name)
    try container.encode(subItemType, forKey: .subItemType)
    try subItem?.encode(to: container.superEncoder())
  }

}
}

(Please note, the above is not meant to be a fully working version, of course :) )

When trying to decode, the compiler throws up the errors:

  • Cannot infer contextual base in reference to member 'subItem'
  • Value of type 'any Decoder' has no member 'decode'

on the line 'self.subItem = try subItemDecoder.decode(SubItem01.self, forKey: .subItem)'

I hope the above makes sense, and if anyone could shed any light, that would be really useful, as it seems a good way to go (I can use multi-inheritance but am trying to expand my understanding).

Thanks in advance,

Jes

3      

It would really, really help if you gave us an example of the JSON you want to work with.

3      

@roosterboy:

It would really, really help if you gave us an example of the JSON you want to work with.

Ok, here is an example of what I mean:

{ 
    "weapons" : [
        {
        "id": 1,
        "name": "Dagger 01",
        "objectType": 1,
        "bladeLength": 5
        },
        {
        "id": 2,
        "name": "Sword 01",
        "objectType": 2,
        "swingFatigue": 3
        },
        {
        "id": 3,
        "name": "Crossbow 01",
        "objectType": 3,
        "reloadTime": 25
        }
        ]
}

So, yes, I could code it like this:

class Weapon: Codable {
    let weapons: [WeaponElement]

    init(weapons: [WeaponElement]) {
        self.weapons = weapons
    }
}

class WeaponElement: Codable {
    let id: Int
    let name: String
    let objectType: Int
    let bladeLength, swingFatigue, reloadTime: Int?

    init(id: Int, name: String, objectType: Int, bladeLength: Int?, swingFatigue: Int?, reloadTime: Int?) {
        self.id = id
        self.name = name
        self.objectType = objectType
        self.bladeLength = bladeLength
        self.swingFatigue = swingFatigue
        self.reloadTime = reloadTime
    }
}

But that's not what I want, as instead of the optional single values (bladeLength, etc) there will be multiple fields for each weapon (again, this is just for demonstrative purposes). I want to be able to do something like:

enum ObjectType {
  case dagger, sword, crossbow
}

protocol subItemProtocol {
}

struct Dagger: subItemProtocol {
  var bladeLength: Int
}

struct Sword: subItemProtocol {
  var swingFatigue: Int
}

struct Crossbow: subItemProtocol {
  var reloadTime: Int
}

class Weapon: Codable {
    let weapons: [WeaponElement]

    init(weapons: [WeaponElement]) {
        self.weapons = weapons
    }
}

class WeaponElement: Codable {
    let id: Int
    let name: String
    let objectType: ObjectType
    let subItem: subItemProtocol

    init(forWeaponAsDagger id: Int, name: String, bladeLength: Int = 5) {
        self.id = id
        self.name = name
        self.objectType = .dagger
        self.subItem = Dagger(bladeLength: bladeLength)
    }

        init(forWeaponAsSword id: Int, name: String, swingFatigue: Int = 3) {
        self.id = id
        self.name = name
        self.objectType = .sword
        self.subItem = Sword(swingFatigue: swingFatigue)
    }

        init(forWeaponAsCrossbow id: Int, name: String, reloadTime: Int = 25) {
        self.id = id
        self.name = name
        self.objectType = .crossbow
        self.subItem = Crossbow(reloadTime: reloadTime)
    }
}

Now, while the code above works in generation of the objects (and seemingly in encoding the objects), when it comes to decoding it fails to compile as described in the original post.

Again, I can get around this by using multiple sub-classes but this seemed like an elegant solution to the problem, so if there's any way that I can do this, that would be great.

3      

Here's one way you could do it, using the article Encoding and decoding polymorphic objects in Swift at the blog Digital Flapjack as a guide:

import Foundation

protocol ItemProtocol: Codable {
    var itemType: ItemType { get }
    var name: String { get set }
    var id: Int { get }
}

enum ItemType: String, Codable {
    case crossbow, dagger, sword

    var metatype: ItemProtocol.Type {
        switch self {
        case .crossbow: return Crossbow.self
        case .dagger: return Dagger.self
        case .sword: return Sword.self
        }
    }
}

struct Dagger: ItemProtocol {
    let itemType = ItemType.dagger
    let id: Int
    var name: String
    var bladeLength: Int
}

struct Sword: ItemProtocol {
    let itemType = ItemType.sword
    let id: Int
    var name: String
    var swingFatigue: Int
}

struct Crossbow: ItemProtocol {
    let itemType = ItemType.crossbow
    let id: Int
    var name: String
    var reloadTime: Int
}

struct ItemWrapper {
    var item: ItemProtocol
}

extension ItemWrapper: Codable {
    private enum CodingKeys: CodingKey {
        case item, itemType
    }

    init(from decoder: Decoder) throws {
        let container = try decoder.container(keyedBy: CodingKeys.self)
        let type = try container.decode(ItemType.self, forKey: .itemType)
        self.item = try type.metatype.init(from: container.superDecoder(forKey: .item))
    }

    func encode(to encoder: Encoder) throws {
        var container = encoder.container(keyedBy: CodingKeys.self)
        try container.encode(item.itemType, forKey: .itemType)
        try item.encode(to: container.superEncoder(forKey: .item))
    }
}

class Armory: Codable {
    var weapons: [ItemProtocol]

    private enum CodingKeys: CodingKey {
        case weapons
    }

    init(weapons: [ItemProtocol]) {
        self.weapons = weapons
    }

    required init(from decoder: Decoder) throws {
        let container = try decoder.container(keyedBy: CodingKeys.self)
        let wrappedItems = try container.decode([ItemWrapper].self, forKey: .weapons)
        self.weapons = wrappedItems.map(\.item)
    }

    func encode(to encoder: Encoder) throws {
        let wrappedItems = weapons.map { ItemWrapper(item: $0) }
        var container = encoder.container(keyedBy: CodingKeys.self)
        try container.encode(wrappedItems, forKey: .weapons)
    }
}

let armory = Armory(weapons: [Dagger(id: 1, name: "Dagger 01", bladeLength: 5),
                              Sword(id: 2, name: "Sword 01", swingFatigue: 3),
                              Crossbow(id: 3, name: "Crossbow 01", reloadTime: 25)])

do {
    let encoder = JSONEncoder()
    encoder.outputFormatting = .prettyPrinted
    let jsonData = try encoder.encode(armory)
    let jsonString = String(data: jsonData, encoding: .utf8)!
    print(jsonString)

    if let newArmory = try? JSONDecoder().decode(Armory.self, from: jsonData) {
        dump(newArmory)
    }
} catch {
    print(error)
}

You end up with JSON that is slightly different from what you posted above, but I think it still holds to the spirit of what you're trying to do.

{
  "weapons" : [
    {
      "item" : {
        "bladeLength" : 5,
        "id" : 1,
        "itemType" : "dagger",
        "name" : "Dagger 01"
      },
      "itemType" : "dagger"
    },
    {
      "item" : {
        "id" : 2,
        "itemType" : "sword",
        "name" : "Sword 01",
        "swingFatigue" : 3
      },
      "itemType" : "sword"
    },
    {
      "item" : {
        "id" : 3,
        "itemType" : "crossbow",
        "name" : "Crossbow 01",
        "reloadTime" : 25
      },
      "itemType" : "crossbow"
    }
  ]
}

4      

Here's another option, to use an array of an enum type that holds each item as its associated value. It's not quite the same thing as conforming a common protocol to Codable, but it's a pretty common way that people do polymorphic Codable conformances.

import Foundation

protocol ItemProtocol {
    var id: Int { get }
    var name: String { get set }
}

enum Item: Codable {
    case crossbow(Crossbow)
    case dagger(Dagger)
    case sword(Sword)

    private enum CodingKeys: CodingKey {
        case item, itemType
    }

    struct InvalidTypeError: Error {
        var type: String
    }

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

        switch itemType {
        case "crossbow":
            self = .crossbow(try container.decode(Crossbow.self, forKey: .item))
        case "dagger":
            self = .dagger(try container.decode(Dagger.self, forKey: .item))
        case "sword":
            self = .sword(try container.decode(Sword.self, forKey: .item))
        default:
            throw InvalidTypeError(type: itemType)
        }
    }

    func encode(to encoder: Encoder) throws {
        var container = encoder.container(keyedBy: CodingKeys.self)
        switch self {
        case .crossbow(let item):
            try container.encode("crossbow", forKey: .itemType)
            try container.encode(item, forKey: .item)
        case .dagger(let item):
            try container.encode("dagger", forKey: .itemType)
            try container.encode(item, forKey: .item)
        case .sword(let item):
            try container.encode("sword", forKey: .itemType)
            try container.encode(item, forKey: .item)
        }
    }
}

struct Crossbow: ItemProtocol, Codable {
    let id: Int
    var name: String
    var reloadTime: Int
}

struct Dagger: ItemProtocol, Codable {
    let id: Int
    var name: String
    var bladeLength: Int
}

struct Sword: ItemProtocol, Codable {
    let id: Int
    var name: String
    var swingFatigue: Int
}

class Armory: Codable {
    var weapons: [Item]

    enum CodingKeys: CodingKey {
        case weapons
    }

    init(weapons: [Item]) {
        self.weapons = weapons
    }

    required init(from decoder: Decoder) throws {
        let container = try decoder.container(keyedBy: CodingKeys.self)
        self.weapons = try container.decode([Item].self, forKey: .weapons)
    }

    func encode(to encoder: Encoder) throws {
        var container = encoder.container(keyedBy: CodingKeys.self)
        try container.encode(weapons, forKey: .weapons)
    }
}

let armory = Armory(weapons: [.dagger(Dagger(id: 1, name: "Dagger 01", bladeLength: 5)),
                              .sword(Sword(id: 2, name: "Sword 01", swingFatigue: 3)),
                              .crossbow(Crossbow(id: 3, name: "Crossbow 01", reloadTime: 25))])

do {
    let encoder = JSONEncoder()
    encoder.outputFormatting = .prettyPrinted
    let jsonData = try encoder.encode(armory)
    let jsonString = String(data: jsonData, encoding: .utf8)!
    print(jsonString)

    if let newArmory = try? JSONDecoder().decode(Armory.self, from: jsonData) {
        dump(newArmory)
    }
} catch {
    print(error)
}

(To be honest you probably don't even need ItemProtocol here, but I left it in anyway.)

And this gives us a JSON representation like this, which nicely leaves out the duplication of the itemType information that the first solution had:

{
  "weapons" : [
    {
      "item" : {
        "id" : 1,
        "name" : "Dagger 01",
        "bladeLength" : 5
      },
      "itemType" : "dagger"
    },
    {
      "item" : {
        "id" : 2,
        "name" : "Sword 01",
        "swingFatigue" : 3
      },
      "itemType" : "sword"
    },
    {
      "item" : {
        "id" : 3,
        "name" : "Crossbow 01",
        "reloadTime" : 25
      },
      "itemType" : "crossbow"
    }
  ]
}

There are probably other ways of handling this situation besides the two I posted, but I'll stop here.

4      

@roosterboy:

Wow, thanks so much for both of those, I'll give them a whirl. I'm not concerned about the actual format of the json, rather the ability to have variable data types embedded without too much of an overhead - your solutions have given me much to work with, thanks x

3      

Hi All, (and especially @roosterboy)

Here is the solution that I came up with which resolves the issues I was having. Essentially the protocol conforming objects (dagger, etc) now have their data contained in, and extracted from, a string value when saved and loaded, and keeps the generic properties (id, name, etc) at the higher level.

This allows the use of non-concrete properties to be used within Codable conforming objects, and - I think - satisfying my initial requirement for Composition over Inheritance, allowing extensibility of objects without the need for subclassing.

(I'm sure there there'll be concerns about the approach (and I'll be happy to hear them) but I'm just glad to have got this far :) )

Again, huge thanks to @roosterboy!

import Foundation

enum ObjectType: Int, Codable {
    case dagger, sword, crossbow
}

protocol ObjectDetailsProtocol: Codable {
    func getDetails() -> String
}

class Dagger: ObjectDetailsProtocol {

    var bladeLength: Int

    required init(bladeLength: Int) {
        self.bladeLength = bladeLength
    }

    init(details: String) {
      let jsonData = details.data(using: .utf8)!
      let decoder = JSONDecoder()
      let info = try! decoder.decode(Dagger.self, from: jsonData)

      bladeLength = info.bladeLength
    }

    func getDetails() -> String {
        return "BladeLength: \(bladeLength)"
    }

    enum CodingKeys: String, CodingKey {
        case bladeLength
    }

    func encode(to encoder: Encoder) throws {
        var container = encoder.container(keyedBy: CodingKeys.self)
        try container.encode(bladeLength, forKey: .bladeLength)
    }

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

class Sword: ObjectDetailsProtocol, Codable {

    var swingFatigue: Int

    required init(swingFatigue: Int) {
        self.swingFatigue = swingFatigue
    }

    init(details: String) {
        let jsonData = details.data(using: .utf8)!
        let decoder = JSONDecoder()
        let info = try! decoder.decode(Sword.self, from: jsonData)

        swingFatigue = info.swingFatigue
    }

    func getDetails() -> String {
        return "Swing Fatigue: \(swingFatigue)"
    }

    enum CodingKeys: String, CodingKey {
        case swingFatigue
    }

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

    func encode(to encoder: Encoder) throws {
        var container = encoder.container(keyedBy: CodingKeys.self)
        try container.encode(swingFatigue, forKey: .swingFatigue)
    }
}

class Crossbow: ObjectDetailsProtocol, Codable {

    var reloadTime: Int

    required init(reloadTime: Int) {
        self.reloadTime = reloadTime
    }

    init(details: String) {
        let jsonData = details.data(using: .utf8)!
        let decoder = JSONDecoder()
        let info = try! decoder.decode(Crossbow.self, from: jsonData)

        reloadTime = info.reloadTime
    }

    func getDetails() -> String {
        return "Reload Time \(reloadTime)"
    }

    enum CodingKeys: String, CodingKey {
        case reloadTime
    }

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

    func encode(to encoder: Encoder) throws {
        var container = encoder.container(keyedBy: CodingKeys.self)
        try container.encode(reloadTime, forKey: .reloadTime)
    }
}

class Object: Codable {

    var id: UUID
    var name: String
    var objectType: ObjectType
    var objectDetails: ObjectDetailsProtocol

    init(forObjectAsDagger name: String = "Dagger", bladeLength: Int = 5) {
        self.id = UUID()
        self.name = name
        self.objectType = .dagger
        self.objectDetails = Dagger(bladeLength: bladeLength)
    }

    init(forObjectAsSword name: String = "Sword", swingFatigue: Int = 5) {
        self.id = UUID()
        self.name = name
        self.objectType = .sword
        self.objectDetails = Sword(swingFatigue: swingFatigue)
    }

    init(forObjectAsCrossbow name: String = "Crossbow", reloadTime: Int = 5) {
        self.id = UUID()
        self.name = name
        self.objectType = .crossbow
        self.objectDetails = Crossbow(reloadTime: reloadTime)
    }

    func getDetails() {
        print("ID: \(id), Name: \(name), Object Type: \(objectType), Object Details: \(objectDetails.getDetails())")
    }

    enum CodingKeys: String, CodingKey {
        case id
        case name
        case objectType
        case objectDetails
    }

    required init(from decoder: Decoder) throws {
        let container = try decoder.container(keyedBy: CodingKeys.self)
        id = try container.decode(UUID.self, forKey: .id)
        name = try container.decode(String.self, forKey: .name)
        objectType = try container.decode(ObjectType.self, forKey: .objectType)

        let detailsString = try container.decode(String.self, forKey: .objectDetails)

        switch objectType {
        case .dagger:
            objectDetails = Dagger(details: detailsString)
        case .sword:
            objectDetails = Sword(details: detailsString)
        case .crossbow:
            objectDetails = Crossbow(details: detailsString)
        }
    }

    func encode(to encoder: Encoder) throws {

        var container = encoder.container(keyedBy: CodingKeys.self)
        try container.encode(id, forKey: .id)
        try container.encode(name, forKey: .name)
        try container.encode(objectType, forKey: .objectType)

        var detailsJSON = ""

        switch objectType {

        case .dagger:
          let dagger = objectDetails as! Dagger
          detailsJSON = String(data: try! JSONEncoder().encode(dagger), encoding: .utf8)!

        case .sword:
          let sword = objectDetails as! Sword
          detailsJSON = String(data: try! JSONEncoder().encode(sword), encoding: .utf8)!

        case .crossbow:
          let crossbow = objectDetails as! Crossbow
          detailsJSON = String(data: try! JSONEncoder().encode(crossbow), encoding: .utf8)!
        }

        try container.encode(detailsJSON, forKey: .objectDetails)

    }

}

class Inventory: Codable {

    var objects = [Object]()

    func addDagger(name: String = "Dagger", bladeLength: Int = 5) {
        let dagger = Object(forObjectAsDagger: name, bladeLength: bladeLength)
        objects.append(dagger)
    }

    func addSword(name: String = "Sword", swingFatigue: Int = 5) {
        let sword = Object(forObjectAsSword: name, swingFatigue: swingFatigue)
        objects.append(sword)
    }

    func addCrossbow(name: String = "Crossbow", reloadTime: Int = 5) {
        let crossbow = Object(forObjectAsCrossbow: name, reloadTime: reloadTime)
        objects.append(crossbow)
    }

    enum CodingKeys: String, CodingKey {
        case objects
    }

    init(){

    }

    required init(from decoder: Decoder) throws {
        let container = try decoder.container(keyedBy: CodingKeys.self)
        objects = try container.decode([Object].self, forKey: .objects)
    }

    func encode(to encoder: Encoder) throws {
        var container = encoder.container(keyedBy: CodingKeys.self)
        try container.encode(objects, forKey: .objects)
    }
}

func main() {

    let inventory = Inventory()

    inventory.addDagger(bladeLength: 1)
    inventory.addDagger(bladeLength: 2)
    inventory.addDagger(bladeLength: 4)
    inventory.addDagger(bladeLength: 8)
    inventory.addDagger(bladeLength: 16)
    inventory.addSword()
    inventory.addCrossbow()

    print("\nInventory:")
    for object in inventory.objects {
      print(object.getDetails())
    }

    let encoder = JSONEncoder()
    let jsonData = try! encoder.encode(inventory)
    let json = String(data: jsonData, encoding: .utf8)!

    print("Json = \(json)")

    // Clear inventory
    inventory.objects.removeAll()

    print("\nCleared Inventory:")
    for object in inventory.objects {
      print(object.getDetails())
    }

    // Decode JSON back into inventory
    let decoder = JSONDecoder()
    let decoded = try! decoder.decode(Inventory.self, from: jsonData)
    inventory.objects = decoded.objects

    // Print decoded inventory
    print("\nDecoded Inventory:")
    for object in inventory.objects {
      print(object.getDetails())
    }
}

3      

Save 50% in my WWDC sale.

SAVE 50% All our books and bundles are half price for Black Friday, so you can take your Swift knowledge further without spending big! Get the Swift Power Pack to build your iOS career faster, get the Swift Platform Pack to builds apps for macOS, watchOS, and beyond, or get the Swift Plus Pack to learn advanced design patterns, testing skills, and more.

Save 50% on all our books and bundles!

Archived topic

This topic has been closed due to inactivity, so you can't reply. Please create a new topic if you need to.

All interactions here are governed by our code of conduct.

 
Unknown user

You are not logged in

Log in or create account
 

Link copied to your pasteboard.