[Codable] Convenient handling of AnyValue collections

Codable with JSON usually works fine – as long as you have homogenous arrays and a strict format.
Sometimes though, the format needs to be a bit more dynamic – often mixed in with strict parts.
In that cases, I settled on using the nice AnyValue
package to decode the dynamic parts.

So far, so good. Now the trouble starts when converting these (nested) collections of AnyValue to "real" values. Let's say I have a simple nested array of Ints.

let json = """
{
    "type": "matrix",
    "matrix": [[101, 4, 106, 174], [186, 238, 234]]
}
"""

Note that sometimes this object has completely different parameters. Through the "type" attribute I usually know what to look for in the rest of the object.

Now what I'd like to have is a simple way to convert these into a static structure, such as:

do {
    let matrix: [[Int32]] = jsonMatrix.value()
    print(matrix)
} catch {
    print("error: \(error)")
}

Rather than having to write custom code for every dynamic part, I tried to come up with a dynamic (sic!) solution, but I am struggling when it comes to arbitrary nesting.

Using the aforementioned AnyValue, I extended this like that:

extension AnyValue {
    
    enum Error: Swift.Error {
        case typeMismatch(info: String)
        case outOfBounds(info: String)
    }
    
    func value<T>() throws -> T {
        switch T.self {
            case is String.Type:
                guard case let .string(string) = self else { throw Error.typeMismatch(info: "Expected \(T.self), got \(self) instead") }
                return string as! T
            case is UInt8.Type:
                guard case let .int(int) = self else { throw Error.typeMismatch(info: "Expected \(T.self), got \(self) instead") }
                guard 0...255 ~= int else { throw Error.outOfBounds(info: "Can't represent (self) as UInt8") }
                return UInt8(int) as! T
            case is Int32.Type:
                guard case let .int(int) = self else { throw Error.typeMismatch(info: "Expected \(T.self), got \(self) instead") }
                return Int32(int) as! T
            case is Int.Type:
                guard case let .int(int) = self else { throw Error.typeMismatch(info: "Expected \(T.self), got \(self) instead") }
                return int as! T
            case is Array<Int32>.Type:
                guard case let .array(array) = self else { throw Error.typeMismatch(info: "Expected \(T.self), got \(self) instead") }
                return try (array.map { try $0.value() as Int32 }) as! T
            case is Array<Array<Int32>>.Type:
                guard case let .array(array) = self else { throw Error.typeMismatch(info: "Expected \(T.self), got \(self) instead") }
                return try (array.map { try $0.value() as [Int32] }) as! T
            default:
                throw Error.typeMismatch(info: "No idea how to deal with \(T.self), while I'm \(self)")
            
        }
    }
}    

I could not find a way to inspect the container types (or decompose the generic type into parts) in a way so that I could decode recursively. Hence, this is neither complete nor that much better than decoding by hand. Is there a better way to deal with a problem like that?

Unless the types are truly unbounded, you can model this with an enum, using the type value as a discriminator in init(from:).

enum Value {
  case matrix([[Int]])
  case string(String)
  etc.
}

There do exist various AnyCodable libraries, or JSON enums which can then be used as a base to transform to other types, rather than being fully dynamic.

Terms of Service

Privacy Policy

Cookie Policy