Runtime Error using Codable

Hi, I’m trying to create a cache in Swift based on a dictionary with String keys and Any values. This is my code:

//
//  Cache.swift
//  GoalPilot
//
//  Created by Rune Pollet on 22/06/2024.
//

import Foundation
import SwiftData

@Model
final class Cache {
    
    private var cache = CodableDictionary()
    
    init() {}
    
    subscript(key: String) -> (any Codable)? {
        get { cache[key] }
        set { cache[key] = newValue }
    }
}

// Fetch support
extension Cache {
    @Transient
    static func get(from context: ModelContext) -> PersistentCache {
        var descriptor = FetchDescriptor<PersistentCache>()
        descriptor.fetchLimit = 1
        
        let cache = try? context.fetch(descriptor).first
        guard let cache else {
            let newCache = PersistentCache()
            context.insert(newCache)
            return newCache
        }
        
        return cache
    }
}

// Dictionary wrapper to make it codable
struct CodableDictionary: Codable {
    
    private var dictionary: [String : AnyCodable] = [:]
    
    init(_ dictionary: [String : AnyCodable] = [:]) {
        self.dictionary = dictionary
    }
    
    init(from decoder: any Decoder) throws {
        let container = try decoder.singleValueContainer()
        let dictionary = try container.decode([String : AnyCodable].self)
        self.dictionary = dictionary
    }
    
    func encode(to encoder: any Encoder) throws {
        var container = encoder.singleValueContainer()
        try container.encode(self.dictionary)
    }
    
    subscript(key: String) -> (any Codable)? {
        get { dictionary[key]?.value }
        set { dictionary[key] = AnyCodable(newValue) }
    }
    
}


// Wrapper to make Any conform to codable
struct AnyCodable: Codable {
    
    enum AnyCodableError: Error {
        case unsupportedType
    }
    
    enum PossibleValue: Int {
        case string = 1, int, double, float, bool, array, dict
        
        var type: any Codable.Type {
            switch self {
            case .string:
                return String.self
            case .int:
                return Int.self
            case .double:
                return Double.self
            case .float:
                return CGFloat.self
            case .bool:
                return Bool.self
            case .array:
                return [AnyCodable].self
            case .dict:
                return [String : AnyCodable].self
            }
        }
        
        static func get(from codableType: any Codable) -> PossibleValue? {
            switch codableType {
            case is String:
                return .string
            case is Int:
                return .int
            case is Double:
                return .double
            case is CGFloat:
                return .float
            case is Bool:
                return .bool
            case is [PossibleValue]:
                return .array
            case is [String : AnyCodableError]:
                return .dict
            default:
                return nil
            }
        }
    }
    
    var value: any Codable
    var valueType: PossibleValue

    init?(_ value: (any Codable)?) {
        if let value, let valueType = PossibleValue.get(from: value) {
            self.value = value
            self.valueType = valueType
        } else {
            return nil
        }
    }

    init(from decoder: Decoder) throws {
        let container = try decoder.singleValueContainer()
        
        // Fetch the type of the value
        let rawValue = try container.decode(Int.self)
        let type = PossibleValue(rawValue: rawValue)
        guard let type else { throw AnyCodableError.unsupportedType }
        self.valueType = type
        
        // Fetch the value
        let value = try container.decode(type.type.self)
        self.value = value
    }

    func encode(to encoder: Encoder) throws {
        var container = encoder.singleValueContainer()
        try container.encode(valueType.rawValue)
        try container.encode(value)
    }
    
}

I have no compile errors, however when using the cache in my app, I get an EXC_BREAKPOINT error on this line of code in the init(from decoder: Decoder) initializer of AnyCodable:

let rawValue = try container.decode(Int.self)

I don’t know why this happens or how I could fix this, all help is welcome!
-Rune

You have the following code at the start of the AnyCodable init that takes a decoder as an argument:

let container = try decoder.singleValueContainer()
let rawValue = try container.decode(Int.self)

In the call to get the raw value from container, the code is trying to decode the value of container into an integer. If the value is not an integer, the code raises an exception, which is what you are seeing. The cause of the error is that container does not have an integer value.

What is the value of container? If you set a breakpoint at the following line of code:

let rawValue = try container.decode(Int.self)

And run the project, Xcode will pause the app when it reaches that line of code. You can check the value of container when the app pauses.

Thanks for your replay @SwiftDevJournal!

Recently after I posted this, I figured there was another better approach for a cache in my app so this code is no longer needed… sorry to fail to inform you about this.

Have a nice day!