skip to Main Content

I am having issues with decoding JSON, where some of the fields returned from the server as not available on the client. Take a look at the following code. The JSON consists of three roles but the Role enum only consists of student and staff. How can I successfully decode JSON ignoring the missing faculty role.

let json = """
[
{
   "role": "student"
},
{
   "role": "staff"
},
{
   "role": "faculty"
},
]
"""

struct User: Decodable {
    let role: Role
}

enum Role: String, Decodable {
    case student
    case staff
    
    private enum CodingKeys: String, CodingKey {
        case student = "student"
        case staff = "staff"
    }
}

let decoded = try! JSONDecoder().decode([User].self, from: json.data(using: .utf8)!)
print(decoded)

Currently, I get the following error:

error: 'try!' expression unexpectedly raised an error: Swift.DecodingError.dataCorrupted(Swift.DecodingError.Context(codingPath: [_JSONKey(stringValue: "Index 2", intValue: 2), CodingKeys(stringValue: "role", intValue: nil)], debugDescription: "Cannot initialize Role from invalid String value faculty", underlyingError: nil))

2

Answers


  1. It will always throw that error because the decoder will be presented with "role": "faculty" which it won’t recognize. Thus if you really really want to ignore it you can ditch the decoder and do something like this:

    guard let json = try? JSONSerialization.jsonObject(with: json.data(using: .utf8)!, options: .mutableContainers) as? [[String: Any]] else {return}
    let users: [User] = json.filter({$0["role"] != "faculty")}.map({User(role: Role(rawValue: $0["role"])!)})
    

    Or you can add the missing case and filter the array:

    users = users.filter({$0 != Role.faculty})
    
    Login or Signup to reply.
  2. You’ll need to create a top-level object to hold the [User]:

    struct UserList {
        var users: [User]
    }
    

    Then, give it a custom decoder that will check for malformed role keys and ignore those Users, but won’t ignore other errors:

    extension UserList: Decodable {
        // An empty object that can decode any keyed container by ignoring all keys
        private struct Ignore: Decodable {}
    
        init(from decoder: Decoder) throws {
            var container = try decoder.unkeyedContainer()
    
            var users: [User] = []
    
            while !container.isAtEnd {
                do {
                    // Try to decode a User
                    users.append(try container.decode(User.self))
                }
                // Check for the specific failure of "role" not decoding.
                // Other errors will still be thrown normally
                catch DecodingError.dataCorrupted(let context)
                            where context.codingPath.last?.stringValue == "role" {
                    // You still need to decode the object into something, but you can ignore it.
                    _ = try container.decode(Ignore.self)
                }
            }
    
            self.users = users
        }
    }
    
    let decoded = try JSONDecoder().decode(UserList.self,
                                           from: json.data(using: .utf8)!).users
    // [User(role: Role.student), User(role: Role.staff)]
    

    If you define User.CodingKeys, then you can skip the Ignore struct:

    struct User: Decodable {
        // The auto-generated CodingKeys is private, so you have to
        // define it by hand to access it elsewhere
        enum CodingKeys: String, CodingKey {
            case role
        }
        let role: Role
    }
    

    With that, you can get rid of the Ignore struct and replace this line:

    _ = try container.decode(Ignore.self)
    

    with:

    _ = try container.nestedContainer(keyedBy: User.CodingKeys.self)
    
    Login or Signup to reply.
Please signup or login to give your own answer.
Back To Top
Search