Search code examples
iosswiftdecodablejsondecoder

Swift: Constant in Template Definition


I'm working with a backend developer that likes to encapsulate json bodies in another object such as data:

Example:

GET: /user/current:

{
  data: {
          firstName: "Evan",
          lastName: "Stoddard"
        }
}

I would simply like to just call json decode on the response to get a User struct that I've created but the added data object requires another struct. To get around this I created a generic template class:

struct DecodableData<DecodableType:Decodable>:Decodable {

    var data:DecodableType

}

Now I can get my json payload and if I want to get a User struct just get the data property of my template:

let user = JSONDecoder().decode(DecodableData<User>.self, from: jsonData).data

This is all fine and dandy until sometimes, the key, data, isn't always data.

I feel like this is most likely fairly trivial stuff, but is there a way I can add a parameter in my template definition so I can change the enum coding keys as that data key might change?

Something like the following?

struct DecodableData<DecodableType:Decodable, Key:String>:Decodable {

    enum CodingKeys: String, CodingKey {
        case data = Key
    }

    var data:DecodableType

}

This way I can pass in the target decodable class along with the key that encapsulates that object.


Solution

  • No need for coding keys. Instead, you need a simple container that parses the JSON as a dictionary that has exactly one key-value pair, discarding the key.

    struct Container<T>: Decodable where T: Decodable {
        let value: T
    
        init(from decoder: Decoder) throws {
            let container = try decoder.singleValueContainer()
            let dict = try container.decode([String: T].self)
    
            guard dict.count == 1 else {
                throw DecodingError.dataCorruptedError(in: container, debugDescription: "expected exactly 1 key value pair, got \(dict.count)")
            }
    
            value = dict.first!.value
        }
    }
    

    If the JSON is empty or has more than one key-value pair, an exception is raised.

    Assuming a simple struct such as

    struct Foo: Decodable, Equatable {
        let a: Int
    }
    

    you can parse it regardless of the key:

    let foo1 = try! JSONDecoder().decode(
        Container<Foo>.self,
        from: #"{ "data": { "a": 1 } }"#.data(using: .utf8)!
    ).value
    
    let foo2 = try! JSONDecoder().decode(
        Container<Foo>.self,
        from: #"{ "doesn't matter at all": { "a": 1 } }"#.data(using: .utf8)!
    ).value
    
    foo1 == foo2 // true
    

    This also works for JSON responses that have null as the value, in which case you need to parse it as an optional of your type:

    let foo = try! JSONDecoder().decode(
        Container<Foo?>.self,
        from: #"{ "data": null }"#.data(using: .utf8)!
    ).value // nil