JSON Parsing using Decodable

2020-03-30 07:49发布

问题:

I am trying to parse the following JSON using decodable protocol. I am able to parse string value such as roomName. But I am not able to map/parse owners, admins, members keys correctly. For eg, using below code, I can able to parse if the values in owners/members are coming as an array. But in some cases, the response will come as a string value(see owners key in JSON), but I am not able to map string values.

Note: Values of admins, members, owners can be string or array (see owners and members keys in JSON)

{
    "roomName": "6f9259d5-62d0-3476-6601-8c284a0b7dde",
    "owners": { 
        "owner": "anish@local.mac" //This can be array or string
    },
    "admins": null, //This can be array or string
    "members": {
        "member": [ //This can be array or string
            "steve@local.mac",
            "mahe@local.mac"
        ]
    }
}

Model:

 struct ChatRoom: Codable{
        var roomName: String! = ""
        var owners: Owners? = nil
        var members: Members? = nil
        var admins: Admins? = nil

        enum RoomKeys: String, CodingKey {
            case roomName
            case owners
            case members
            case admins
        }
       init(from decoder: Decoder) throws {
            let container = try decoder.container(keyedBy: RoomKeys.self)
            roomName = try container.decode(String.self, forKey: .roomName)
           if let member = try? container.decode(Members.self, forKey: .members) {
                members = member
            }
            if let owner = try? container.decode(Owners.self, forKey: .owners) {
                owners = owner
            }
            if let admin = try? container.decode(Admins.self, forKey: .admins) {
                admins = admin
            }
    }
}

//Owner Model

struct Owners:Codable{
    var owner: AnyObject?

    enum OwnerKeys:String,CodingKey {
        case owner
    }

    init(from decoder: Decoder) throws {
        let container = try decoder.container(keyedBy: OwnerKeys.self)
        if let ownerValue = try container.decodeIfPresent([String].self, forKey: .owner){
            owner = ownerValue as AnyObject
        }
        else{
            owner = try? container.decode(String.self, forKey: .owner) as AnyObject
        }
    }

    func encode(to encoder: Encoder) throws {

    }
}

//Member Model

struct Members:Codable{
    var member:AnyObject?

    enum MemberKeys:String,CodingKey {
        case member
    }

    init(from decoder: Decoder) throws {
        let container = try decoder.container(keyedBy: MemberKeys.self)
        if let memberValue = try container.decodeIfPresent([String].self, forKey: .member){
            member = memberValue as AnyObject
        }
        else{
            member = try? container.decode(String.self, forKey: .member) as AnyObject
        }
    }

    func encode(to encoder: Encoder) throws {

    }
}

回答1:

This should work. I've removed Admin model for simplicity. I'd prefer Owners/Members to be arrays as they can have one or more values which is what they're for, but if you want them to be AnyObject, you can cast them as so like you're already doing in your init(decoder:).

Test data:

var json = """
    {
        "roomName": "6f9259d5-62d0-3476-6601-8c284a0b7dde",
        "owners": {
            "owner": "anish@local.mac"
        },
        "admins": null,
        "members": {
            "member": [
            "steve@local.mac",
            "mahe@local.mac"
            ]
        }
    }
    """.data(using: .utf8)

Models:

struct ChatRoom: Codable, CustomStringConvertible {
    var roomName: String! = ""
    var owners: Owners? = nil
    var members: Members? = nil

    var description: String {
        let encoder = JSONEncoder()
        encoder.outputFormatting = .prettyPrinted
        let data = try? encoder.encode(self)
        return String(data: data!, encoding: .utf8)!
    }

    enum RoomKeys: String, CodingKey {
        case roomName
        case owners
        case members
        case admins
    }
    init(from decoder: Decoder) throws {
        let container = try decoder.container(keyedBy: RoomKeys.self)
        roomName = try container.decode(String.self, forKey: .roomName)
        members = try container.decode(Members.self, forKey: .members)
        owners = try? container.decode(Owners.self, forKey: .owners)
    }
}

struct Owners:Codable{
    var owner: [String]?

    enum OwnerKeys:String,CodingKey {
        case owner
    }

    init(from decoder: Decoder) throws {
        let container = try decoder.container(keyedBy: OwnerKeys.self)
        if let ownerValue = try? container.decode([String].self, forKey: .owner){
            owner = ownerValue
        }
        else if let own = try? container.decode(String.self, forKey: .owner) {
            owner = [own]
        }
    }
}

struct Members: Codable {
    var member:[String]?

    enum MemberKeys:String,CodingKey {
        case member
    }

    init(from decoder: Decoder) throws {
        let container = try decoder.container(keyedBy: MemberKeys.self)
        if let memberValue = try? container.decode([String].self, forKey: .member){
            member = memberValue
        }
        else if let str = try? container.decode(String.self, forKey: .member){
            member = [str]
        }
    }
}

Test:

var decoder = JSONDecoder()
try? print("\(decoder.decode(ChatRoom.self, from: json!))")

Output:

{
  "owners" : {
    "owner" : [
      "anish@local.mac"
    ]
  },
  "members" : {
    "member" : [
      "steve@local.mac",
      "mahe@local.mac"
    ]
  },
  "roomName" : "6f9259d5-62d0-3476-6601-8c284a0b7dde"
}


回答2:

As you are getting some data as Array or String you can parse this underlying Type with the help of an enum. This will reduce some boilerplate codes as well as redundant codes for each Type you define that is able to have Array or String values.

You define an enum like this:

enum ArrayOrStringType: Codable {
    case array([String])
    case string(String)

    init(from decoder: Decoder) throws {
        let container = try decoder.singleValueContainer()
        do {
            self = try .array(container.decode([String].self))
        } catch DecodingError.typeMismatch {
            do {
                self = try .string(container.decode(String.self))
            } catch DecodingError.typeMismatch {
                throw DecodingError.typeMismatch(ArrayOrStringType.self, DecodingError.Context(codingPath: decoder.codingPath, debugDescription: "Encoded payload conflicts with expected type"))
            }
        }
    }

    func encode(to encoder: Encoder) throws {
        var container = encoder.singleValueContainer()
        switch self {
        case .array(let array):
            try container.encode(array)
        case .string(let string):
            try container.encode(string)
        }
    }
}

And then your models go as:

struct ChatRoom: Codable {
    let roomName: String
    let owners: Owner
    let admins: ArrayOrStringType?  // as you are likely to get null values also
    let members: Member

    struct Owner: Codable {
        let owner: ArrayOrStringType
    }
    struct Member: Codable {
        let member: ArrayOrStringType
    }
}
/// See!! No more customization inside every init(from:)

Now you can parse your data that contains any of your desired type (Array, String)

Test data 1:

// owner having String type
let jsonTestData1 = """
{
    "roomName": "6f9259d5-62d0-3476-6601-8c284a0b7dde",
    "owners": {
        "owner": "anish@local.mac"
    },
    "admins": null,
    "members": {
        "member": [
            "steve@local.mac",
            "mahe@local.mac"
        ]
    }
}
""".data(using: .utf8)!

Test data 2:

// owner having [String] type
let jsonTestData2 = """
{
    "roomName": "6f9259d5-62d0-3476-6601-8c284a0b7dde",
    "owners": {
        "owner": ["anish1@local.mac", "anish2@local.mac"]
    },
    "admins": null,
    "members": {
        "member": [
            "steve@local.mac",
            "mahe@local.mac"
        ]
    }
}
""".data(using: .utf8)!

Decoding process:

do {
    let chatRoom = try JSONDecoder().decode(ChatRoom.self, from:jsonTestData1)
    print(chatRoom)
} catch {
    print(error)
}
// will print
{
  "owners" : {
    "owner" : "anish@local.mac"
  },
  "members" : {
    "member" : [
      "steve@local.mac",
      "mahe@local.mac"
    ]
  },
  "roomName" : "6f9259d5-62d0-3476-6601-8c284a0b7dde"
}

do {
    let chatRoom = try JSONDecoder().decode(ChatRoom.self, from:jsonTestData2)
    print(chatRoom)
} catch {
    print(error)
}
// will print
{
  "owners" : {
    "owner" : [
      "anish1@local.mac",
      "anish2@local.mac"
    ]
  },
  "members" : {
    "member" : [
      "steve@local.mac",
      "mahe@local.mac"
    ]
  },
  "roomName" : "6f9259d5-62d0-3476-6601-8c284a0b7dde"
}


You can even get more out of the structure. Lets say, you want to work with owners only. You will likely try to get the values as Swifty way:

do {
    let chatRoom = try JSONDecoder().decode(ChatRoom.self, from:json)
    if case .array(let owners) = chatRoom.owners.owner {
        print(owners) // ["anish1@local.mac", "anish2@local.mac"]
    }
    if case .string(let owners) = chatRoom.owners.owner {
        print(owners) // "anish@local.mac"
    }
} catch {
    print(error)
}

Hope this structuring helps a lot more than other typical ways. Plus this is having the explicit consideration of your expected types. Neither it relies on one type (Array only) nor Any/AnyObject type.



回答3:

I recreated yours models and tested with your JSON and it worked fine. If your backend returns different types in the different cases (business rules), maybe the best way is create separate variables for each case.(imho)

// Model
import Foundation
struct ChatRoom : Codable {
    let roomName : String?
    let owners : Owners?
    let admins : String?
    let members : Members?

    enum CodingKeys: String, CodingKey {

        case roomName = "roomName"
        case owners
        case admins = "admins"
        case members
    }

    init(from decoder: Decoder) throws {
        let values = try decoder.container(keyedBy: CodingKeys.self)
        roomName = try values.decodeIfPresent(String.self, forKey: .roomName)
        owners = try Owners(from: decoder)
        admins = try values.decodeIfPresent(String.self, forKey: .admins)
        members = try Members(from: decoder)
    }

}

-

// Member Model
    import Foundation
    struct Members : Codable {
        let member : [String]?

        enum CodingKeys: String, CodingKey {

            case member = "member"
        }

        init(from decoder: Decoder) throws {
            let values = try decoder.container(keyedBy: CodingKeys.self)
            member = try values.decodeIfPresent([String].self, forKey: .member)
        }

    }

-

// Owner Model

import Foundation
struct Owners : Codable {
    let owner : String?

    enum CodingKeys: String, CodingKey {

        case owner = "owner"
    }

    init(from decoder: Decoder) throws {
        let values = try decoder.container(keyedBy: CodingKeys.self)
        owner = try values.decodeIfPresent(String.self, forKey: .owner)
    }

}