简体   繁体   English

使用Decodable进行JSON解析

[英]JSON Parsing using Decodable

I am trying to parse the following JSON using decodable protocol. 我试图使用可解码协议解析以下JSON。 I am able to parse string value such as roomName . 我能够解析诸如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. 但在某些情况下,响应将作为字符串值(请参阅JSON中的所有者键),但我无法映射字符串值。

Note: Values of admins, members, owners can be string or array (see owners and members keys in JSON) 注意: 管理员,成员,所有者的值可以是字符串或数组(请参阅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 {

    }
}

This should work. 这应该工作。 I've removed Admin model for simplicity. 为简单起见,我删除了Admin模型。 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:) . 我更喜欢Owners / Members是数组,因为它们可以有一个或多个值,这是他们的目的,但是如果你想让它们成为AnyObject,你可以像在你的init(decoder:)那样投射它们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"
}

As you are getting some data as Array or String you can parse this underlying Type with the help of an enum . 当您获取一些数据作为ArrayString您可以借助enum来解析此基础Type 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. 这将减少一些样板代码以及您定义的每个可以具有ArrayString值的Type冗余代码。

You define an enum like this: 你定义这样的enum

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 ) 现在,您可以解析包含任何所需类型的数据( ArrayString

Test data 1: 测试数据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: 测试数据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: 您可能会尝试以Swifty的方式获取值:

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. 它既不依赖于一种类型(仅限Array ),也不依赖于Any / AnyObject类型。

I recreated yours models and tested with your JSON and it worked fine. 我重新创建了你的模型并使用你的JSON进行了测试,它运行良好。 If your backend returns different types in the different cases (business rules), maybe the best way is create separate variables for each case.(imho) 如果你的后端在不同的情况下(业务规则)返回不同的类型,也许最好的方法是为每个案例创建单独的变量。(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)
    }

}

声明:本站的技术帖子网页,遵循CC BY-SA 4.0协议,如果您需要转载,请注明本站网址或者原文地址。任何问题请咨询:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM