skip to Main Content

I’m creating a register view on my app, and I’m trying to decode a json but the key has a different type depending of the response.

If the register success:

{
    "data": true,
    "success": true
}

if the register fails with many reasons:

{
    "data": ["email is not valid", "password must have more than 4 digits"],
    "success": false
}

if the register fails with only one reason:

{
    "data": "email is not valid",
    "success": false
}

How to build a struct in Swift to handle this situations ?

struct Response: Codable {
   var data: // what type to use: String? [String]? Bool?
   var success: Bool
}

I asked the backend dev but for the moment, there’s no changes on the backside, so I have to do with it …

2

Answers


  1. You can introduce an enumeration with all possible options and provide your custom implementation of init(from decoder: Decoder)

    enum DataType: Codable {
        case success(Bool)
        case singleError(String)
        case multiError([String])
        case undefined
    }
    
    struct Response: Codable {
    
        enum CodingKeys: String, CodingKey {
            case data
            case success
        }
    
        var data: DataType
        var success: Bool
    
        init(from decoder: Decoder) throws {
            let container = try decoder.container(keyedBy: CodingKeys.self)
            success = try container.decode(Bool.self, forKey: .success)
        
            if let bool = try? container.decode(Bool.self, forKey: .data) {
                data = DataType.success(bool)
            } else if let string = try? container.decode(String.self, forKey: .data) {
                data = DataType.singleError(string)
            } else if let strings = try? container.decode([String].self, forKey: .data) {
                data = DataType.multiError(strings)
            } else {
                data = .undefined
            }
        }
    }
    

    Alternatively you can just have separate properties in your struct and set them accordingly. Enum just looks more Swifty.

    In case you can ignore Bool as Joakim suggested, then you can simply have that (no need for enum):

    struct Response: Codable {
        
        var data: [String]
        var success: Bool
    
        init(from decoder: Decoder) throws {
            let container = try decoder.container(keyedBy: CodingKeys.self)
            success = try container.decode(Bool.self, forKey: .success)
        
            if let string = try? container.decode(String.self, forKey: .data) {
                data = [string]
            } else if let strings = try? container.decode([String].self, forKey: .data) {
                data = strings
            } else {
                data = []
            }
        }
    }
    
    Login or Signup to reply.
  2. I would define an enum for this result. E.g.,

    enum ResponseResult<Success> {
        case success(Success)
        case failure([String])
    }
    

    And I would then have a custom decoder for this:

    struct ResponseObject<T: Decodable>: Decodable {
        let result: ResponseResult<T>
        
        enum CodingKeys: String, CodingKey {
            case data, success
        }
    
        init(from decoder: Decoder) throws {
            let container = try decoder.container(keyedBy: CodingKeys.self)
            let success = try container.decode(Bool.self, forKey: .success)
            if success {
                result = .success(try container.decode(T.self, forKey: .data))
                return
            }
            
            if !container.contains(.data) {
                result = .failure([])
            } else if let message = try? container.decode(String.self, forKey: .data) {
                result = .failure([message])
            } else if let messages = try? container.decode([String].self, forKey: .data) {
                result = .failure(messages)
            } else if let wasNull = try? container.decodeNil(forKey: .data), wasNull {
                result = .failure([])
            } else {
                throw DecodingError.dataCorruptedError(forKey: .data, in: container, debugDescription: "Expected string, array of strings, null, or no key named 'data'")
            }
        }
    }
    

    Note, you mentioned this as being for this particular endpoint, that returned Bool upon success. But if they’ve done this for the registration endpoint, I bet they’ve done this for other endpoints, too. So, the above is generic, which in this case, you would use like:

    let registrationResponse = try JSONDecoder().decode(ResponseObject<Bool>.self, from: data)
    
    switch registrationResponse.result {
    case .success(let payload):
        print(payload)
    case .failure(let messages):
        print(messages.joined(separator: " ")) 
    }
    

    But it could easily be used with other Success payload types.

    Note, in this case, you probably don’t care about the success payload (i.e., if the registration was successful, then both the success and data keys are likely true; if it wasn’t successful, then the data will contain the error messages), but that is a conversation between you and your backend engineers. But you probably want the “let me extract the payload” pattern for those other endpoints where this inconsistent error message pattern is employed.

    For more information about this custom decoding pattern, see Encoding and Decoding Custom Types.

    Login or Signup to reply.
Please signup or login to give your own answer.
Back To Top
Search