Home > other >  Nested Container nil value custom decoder Swift
Nested Container nil value custom decoder Swift

Time:12-02

So I have this simple json response:

{
    "gender": "male",
    "name": {
        "title": "mr",
        "first": "brad",
        "last": "gibson"
        }
}

And this is my customer decoder:

struct UserModel: Decodable {
    var gender: String
    var title: String?
    var first: String?
    var last: String?
    
    // Top-level coding keys
    enum CodingKeys: String, CodingKey {
        case name, gender
    }
    
    enum NameKeys: CodingKey {
        case title, first, last
    }
    
    init(from decoder: Decoder) throws {
        let container = try decoder.container(keyedBy: CodingKeys.self)
        gender = try container.decode(String.self, forKey: .gender)
        let name = try container.nestedContainer(keyedBy: NameKeys.self, forKey: .name)
        title = try name.decodeIfPresent(String.self, forKey: .title)
        first = try name.decodeIfPresent(String.self, forKey: .first)
        last = try name.decodeIfPresent(String.self, forKey: .last)
    }
}

I know how to handle the error scenarios if one of the keys inside the nested object name (title, first, last) isn't sent. My question is how would I write something to prevent a crash if the "name" object is not sent back from the response?

CodePudding user response:

How about using below?

struct Name: Decodable {

    var title: String?
    var first: String?
    var last: String?

}

struct UserModel: Decodable {
    var name: Name?
    var gender: String
}

Then everything else gets taken care of itself. For e.g all of below will be parsed without issues.

{
    "gender": "male",
    "name": {
        "title": "mr",
        "first": "brad",
        "last": "gibson"
        }
}

{
    "gender": "male"
}

{
    "gender": "male",
    "name": {
        "title": "mr",
        "first": "brad"
        }
}

I tested using below

let str = """
    {
        "gender": "male",
        "name": {
            "title": "mr",
            "first": "brad"
            }
    }
    """

let usermodel = try! JSONDecoder().decode(UserModel.self, from: str.data(using: .utf8)!)
print(usermodel)

CodePudding user response:

You can have nested Codable structs, as such:

struct User: Codable {
    let gender: String
    let name: UserName?
}

struct UserName: Codable {
    let title: String?
    let first: String?
    let last: String?
}

To make the syntax better than user.name?.title, we can change this to just user.title with the use of @dynamicMemberLookup. Change User to this:

@dynamicMemberLookup
struct User: Codable {
    let gender: String
    let name: UserName?

    subscript<T>(dynamicMember k: KeyPath<UserName, T>) -> T? {
        name?[keyPath: k]
    }

    subscript<T>(dynamicMember k: KeyPath<UserName, T?>) -> T? {
        name?[keyPath: k]
    }
}

Now, you can do the short-hand form to get each property. This now mimics your single container with all the properties.

CodePudding user response:

Thanks to Deepika, this is the solution:

struct UserModel: Decodable {
var gender: String
var title: String?
var first: String?
var last: String?

// Top-level coding keys
enum CodingKeys: String, CodingKey {
    case name, gender
}

enum NameKeys: CodingKey {
    case title, first, last
}

init(from decoder: Decoder) throws {
    let container = try decoder.container(keyedBy: CodingKeys.self)
    gender = try container.decode(String.self, forKey: .gender)
    
    if container.contains(.name) {
        let name = try container.nestedContainer(keyedBy: NameKeys.self, forKey: .name)
        title = try name.decode(String.self, forKey: .title)
        first = try name.decode(String.self, forKey: .first)
        last = try name.decode(String.self, forKey: .last)
    }
}

}

  • Related