jsonswiftjsondecoderjson-flattener

swift: How can I decode an array of json objects, without creating a struct, that holds an array of said objects?


My data looks like this:

"places": [
        {
            "id": 15,
            "name": "København",
            "typeId": 6,
            "coordinates": {
                "lat": "55.6760968",
                "lng": "12.5683372"
            },
            "count": 2779
        },
        {
            "id": 19,
            "name": "København S",
            "typeId": 3,
            "coordinates": {
                "lat": "55.6508754",
                "lng": "12.5991891"
            },
            "count": 1168
        }
]

I wish to avoid this:

struct Places: Decodable {
    let places: [Place]
}

Which is suggested by QuickType.io: https://app.quicktype.io?share=j22hopuBnkuHZziOSvxG And instead just decode within "places" list. Such that this would work:

let places = try JSONDecoder().decode([Place].self, from: data)

Possible solutions that I've found so far:

  1. The 'Scuffed' solution: https://stackoverflow.com/a/62403633/13481876

  2. Create generic decodable array struct: https://swiftsenpai.com/swift/decode-dynamic-keys-json/


Solution

  • If you find yourself needing this multiple times, then you can build your own generic struct that decodes over whichever key it finds:

    struct Nester<T: Decodable>: Decodable {
        let elements: [T]
        
        init(from decoder: Decoder) throws {
            let container = try decoder.container(keyedBy: CodingKeys.self)
            if let key = container.allKeys.first {
                elements = try container.decode([T].self, forKey: key)
            } else {
                // we run into an empty dictionary, let's signal this
                throw DecodingError.typeMismatch([String:Any].self, DecodingError.Context(codingPath: [], debugDescription: "Expected to find at least one key"))
            }
        }
        
        // A coding key that accepts whatever string value it is given
        struct CodingKeys: CodingKey {
            let stringValue: String
            var intValue: Int? { nil }
            
            init?(stringValue: String) {
                self.stringValue = stringValue
            }
                    
            init?(intValue: Int) { return nil }
        }
    }
    

    And with this in hand, you can extend JSONDecoder in order to get a nicer call site:

    extension JSONDecoder {
        func decode<T: Decodable>(nested: [T].Type, from data: Data) throws -> [T] {
            try decode(Nester<T>.self, from: data).elements
        }
    }
    

    Then it's just a matter of calling the new overload:

    let places = try JSONDecoder().decode(nested: [Place].self, from: data)
    

    P.S. if you want, you can hide the complex struct within the extension, resulting in something like this:

    extension JSONDecoder {
        func decode<T: Decodable>(nested: [T].Type, from data: Data) throws -> [T] {
            try decode(Nester<T>.self, from: data).elements
        }
        
        private struct Nester<T: Decodable>: Decodable {
            let elements: [T]
            
            init(from decoder: Decoder) throws {
                let container = try decoder.container(keyedBy: CodingKeys.self)
                if let key = container.allKeys.first {
                    elements = try container.decode([T].self, forKey: key)
                } else {
                    throw DecodingError.typeMismatch([String:Any].self, DecodingError.Context(codingPath: [], debugDescription: "Expected to find at least one key"))
                }
            }
            
            struct CodingKeys: CodingKey {
                let stringValue: String
                var intValue: Int? { nil }
                
                init?(stringValue: String) {
                    self.stringValue = stringValue
                }
                        
                init?(intValue: Int) { return nil }
            }
        }
    }
    

    The downside is that you'll not be able to reuse the struct if you want to extend other decoders besides the JSON one.