Subclass not decoding through superclass

I'm having trouble decoding an array of subclassed super classes and getting the correct full subclass instance. When I run tests, the JSON is written correctly but I get a fatal error when trying to read and decode. It says "Thread 1: Fatal error: The data couldn’t be read because it is missing." I'm pretty sure that my nested decoding is incorrect. (At the advice of ChatGPT, I added the "type" attribute to help with know which subclass to actually init.). Currently I have only one subclass of CheckTask but there will be another once I know how to make this work. Here's what I'm attempting so far...

import Foundation
import Combine

class CheckList: Identifiable, ObservableObject, Codable {
    let id: String
    var title: String
    @Published var tasks: [CheckTask]
    var domain: String

....
    
   enum CodingKeys: String, CodingKey {
        case id, title, tasks, domain, type
    }

      required init(from decoder: Decoder) throws {
        let container = try decoder.container(keyedBy: CodingKeys.self)
        id = try container.decode(String.self, forKey: .id)
        title = try container.decode(String.self, forKey: .title)
        domain = try container.decode(String.self, forKey: .domain)
        
        var tasksContainer = try container.nestedUnkeyedContainer(forKey: .tasks)
        var decodedTasks: [CheckTask] = []

        while !tasksContainer.isAtEnd {
            let taskContainer = try tasksContainer.nestedContainer(keyedBy: CheckTask.CodingKeys.self)
            let type = try taskContainer.decode(String.self, forKey: .type)

            switch type {
            case "CheckItem":
                let item = try tasksContainer.decode(CheckItem.self)
                decodedTasks.append(item as CheckTask)
            // Add cases for other subclasses as needed
            default:
                // Handle unknown type or log a warning
                print("Unknown task type: \(type)")
            }
        }
        tasks = decodedTasks
    }

 ....

}


import Foundation

class CheckTask: Identifiable, ObservableObject, Equatable, Codable {
    var id: UUID
    var locked: Bool
    
 ....
    
    required init(from decoder: Decoder) throws {
        let container = try decoder.container(keyedBy: CodingKeys.self)
        id = try container.decode(UUID.self, forKey: .id)
        locked = try container.decode(Bool.self, forKey: .locked)
    }
    
....    
}


import Foundation

class CheckItem: CheckTask {
    var title: String
    @Published var checkmark: CheckMark
    var skippable: Bool = false
    var instructions: String
    var instructionLinks: [InstructionLink]

....

     // CodingKeys to specify custom mapping between Swift names and JSON keys
    enum CodingKeys: String, CodingKey {
        case type, title, checkmark, skippable, complete, instructions, instructionLinks
    }

    // Custom decoding method to decode the properties
    required init(from decoder: Decoder) throws {
        let container = try decoder.container(keyedBy: CodingKeys.self)
        title = try container.decode(String.self, forKey: .title)
        checkmark = try container.decode(CheckMark.self, forKey: .checkmark)
        skippable = try container.decode(Bool.self, forKey: .skippable)
        instructions = try container.decode(String.self, forKey: .instructions)
        instructionLinks = try container.decode([InstructionLink].self, forKey: .instructionLinks)
        try super.init(from: decoder)
    }

....

}

I'm sure that the CheckTask and CheckItem decoding works but it's the CheckList that seems to be the issue. How can I do this correctly? Thanks!

I believe I found a solution to this. The error I was getting was caused by getting the container for the JSON data too many times, using up the data out of step of my intent. I guess it has something to to with how data is streamed from the file into the decoder.

The way out is ...

  1. To break encapsulation a bit by making the CheckTask super class aware of all keys of of any of it's subclasses so that when the decoding process gets a nestedContainer, it's ready to go with all key knowledge whether it uses them all or not. that means the same container can be used for the decoding of any subclass.

and

  1. Each subclass implements an extra init function that takes that container as an argument, then specifically decodes from there.

Here are some pertinent code snippets...

class CheckList: Identifiable, ObservableObject, Codable {
    let id: String
    var title: String
    @Published var tasks: [CheckTask]
    var domain: String

....

    required init(from decoder: Decoder) throws {
        let container = try decoder.container(keyedBy: CodingKeys.self)
        id = try container.decode(String.self, forKey: .id)
        title = try container.decode(String.self, forKey: .title)
        domain = try container.decode(String.self, forKey: .domain)
        
        // get the CheckTasks even though we don't know what each one is yet
        var tasksContainer = try container.nestedUnkeyedContainer(forKey: .tasks)
        
        tasks = []
        
        // Iterate through the array of objects, determine the type, and decode accordingly
        while !tasksContainer.isAtEnd {
            // Because we don't know which subclass yet, we toss all possible keys at the taskContainer.
            // We only get one bite at the apple and it has to be prepared to decode anything.
            // See CheckTask.CodingKeys for more info.
            let taskContainer = try tasksContainer.nestedContainer(keyedBy: CheckTask.CodingKeys.self)
            let type = try taskContainer.decode(CheckType.self, forKey: .type)
            
            // Now that we know the type, we choose how to init the correct class,
            // using a convenience method that takes a container,
            // then append to the array of tasks.
            switch type {
            case .CheckItem:
                let item = try CheckItem(container: taskContainer)
                tasks.append(item)
            default:
                // Handle unknown type or log a warning
                print("Unknown task type:.")
            }
        }
    }

}
class CheckTask: Identifiable, ObservableObject, Equatable, Codable {
    var id: UUID
    var locked: Bool
    var type: CheckType

....

    // CodingKeys to specify custom mapping between Swift names and JSON keys
    // I hate to break encapsulation like this but, hey, that's decoding for ya.
    enum CodingKeys: String, CodingKey {
        // common keys
        case id, locked, type
        // CheckItem keys
        case title, checkmark, skippable, complete, instructions, instructionLinks
        // CheckGroup keys
        case items
    }

    required init(from decoder: Decoder) throws {
        let container = try decoder.container(keyedBy: CodingKeys.self)
        id = try container.decode(UUID.self, forKey: .id)
        locked = try container.decode(Bool.self, forKey: .locked)
        type = try container.decode(CheckType.self, forKey: .type)
    }
}

enum CheckType: Codable {
    case CheckItem
    case CheckGroup
    case Unknown
}
class CheckItem: CheckTask {
    var title: String
    @Published var checkmark: CheckMark
    var skippable: Bool = false
    var instructions: String
    var instructionLinks: [InstructionLink]

....
    
    // Initialize with values
    init(id: UUID, title: String, checkmark: CheckMark, skippable: Bool, locked: Bool, instructions: String?, instructionLinks: [InstructionLink]?) {
        self.title = title
        self.checkmark = checkmark
        self.skippable = skippable
        self.instructions = instructions ?? ""
        self.instructionLinks = instructionLinks ?? []
        super.init(id: id, locked: locked, type: CheckType.CheckItem)
    }

    /// Custom decoding method to decode the properties from a JSON container.  This is used by the CheckList decode
    /// so that it can handle instantiating subclasses of CheckTask blindly since it doesn't know there are subclasses
    /// and can't direct specify a type.
    /// - Parameter container: <#container description#>
    convenience init(container: KeyedDecodingContainer<CodingKeys>) throws {
        self.init(id: try container.decode(UUID.self, forKey: .id),
                  title: try container.decode(String.self, forKey: .title),
                  checkmark: try container.decode(CheckMark.self, forKey: .checkmark),
                  skippable: try container.decode(Bool.self, forKey: .skippable),
                  locked: try container.decode(Bool.self, forKey: .locked),
                  instructions: try container.decode(String.self, forKey: .instructions),
                  instructionLinks: try container.decode([InstructionLink].self, forKey: .instructionLinks)
                  )
    }
}

So now I'll implement another subclass of CheckTask called CheckGroup and see how I can break stuff then.

Subclass not decoding through superclass
 
 
Q