iosjsonswiftuikit

Filtering empty values from an API in swift


I'm trying to filter out empty and null values from an API in a JSON format in swift (UIKit). The full data returns look like below but sometimes can contain null or empty values in the characteristic key. There is always going to be the same amount of keys.

//Cat

{
"breedname": "Persian",
"picture": "https://catimage.random.png",
"characteristic1": "Shy",
"characteristic2": "Hungry all the time"
"characteristic3": "Likes apples"
"characteristic4": "Grey color"
"characteristic5": "likes chin scratches"
 }

{
"breedname": "Bengal",
"picture": "https://catimage.random.png",
"characteristic1": "Active",
"characteristic2": "Adventurous"
"characteristic3": ""
"characteristic4": ""
"characteristic5": ""
 }

{
"breedname": "ragdoll",
"picture": "https://catimage.random.png",
"characteristic1": "Fiestey",
"characteristic2": "sharp claws"
"characteristic3": null
"characteristic4": null
"characteristic5": null
 }

In order to filter null and empty values before showing in the UI, I have a Decodable class like below and a custom init class with the decodeifPresent method which changes null values to nill. However for empty values I just created a method which converts empty string values to nill. I'm not sure if there are better ways to handle empty and null data and filtering them out? I refer to all the Decodable keys in the UI so I cannot simply delete the keys themselves.

struct Cat: Decodable {
    
    let breedName: String
    let picture: String
    let characteristic1 : String?
    let characteristic2 : String?
    let characteristic3 : String?
    let characteristic4 : String?
    let characteristic5 : String?

enum CodingKeys: String, CodingKey {
        case breedName
        case picture
        case characteristic1
        case characteristic2
        case characteristic3
        case characteristic4
        case characteristic5
}

    func checkEmpty(s: String?) -> String? {
        if s == "" {
            return nil
        }
        return s
    }

init(from decoder: Decoder) throws {
        let container = try decoder.container(keyedBy: CodingKeys.self)

        self.breedName= try container.decode(String.self, forKey: .breedName)
        self.picture = try container.decode(String.self, forKey: .picture)
        self.characteristic1 = try container.decodeIfPresent(String.self, forKey: .characteristic1)

        self.characteristic2 = try container.decodeIfPresent(String.self, forKey: .characteristic2)
        self.characteristic3 = try container.decodeIfPresent(String.self, forKey: .characteristic3)
        self.characteristic4 = try container.decodeIfPresent(String.self, forKey: .characteristic4)
        self.characteristic5 = try container.decodeIfPresent(String.self, forKey: .characteristic5)

self.characteristic1 = checkEmpty(s: self.characteristic1)
self.characteristic2 = checkEmpty(s: self.characteristic2)
self.characteristic3 = checkEmpty(s: self.characteristic3)
self.characteristic4 = checkEmpty(s: self.characteristic4)
self.characteristic5 = checkEmpty(s: self.characteristic5)

Solution

  • One solution is to check for empty in a function defined in an extension to String

    extension String {
        func emptyAsNil() -> String? {
            self.isEmpty ? nil : self
        }
    }
    

    Then you could do all in one step in the init

    self.characteristic1 = try container.decodeIfPresent(String.self, forKey: .characteristic1)?.emptyAsNil()
    

    But perhaps a better solution is to gather all those properties in a collection like an array or a dictionary. Here I have chosen an array

    struct Cat: Decodable {
        let breedName: String
        let picture: String
        var characteristics: [String]
    }
    

    and then in the init we add only non-nil, non-empty values to the array

    if let value = try container.decodeIfPresent(String.self, forKey: .characteristic1), !value.isEmpty {
        characteristics.append(value)
    }
    

    or another way is to loop over the keys

    let keys: [CodingKeys] = [.characteristic1,
                                  .characteristic2,
                                  .characteristic3,
                                  .characteristic4,
                                  .characteristic5]
    
    for key in keys {
        if let value = try container.decodeIfPresent(String.self, forKey: key), !value.isEmpty {
            characteristics.append(value)
        }
    }