JSON Parsing using Decodable

匿名 (未验证) 提交于 2019-12-03 10:24:21

问题:

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)     }  } 


标签
易学教程内所有资源均来自网络或用户发布的内容,如有违反法律规定的内容欢迎反馈
该文章没有解决你所遇到的问题?点击提问,说说你的问题,让更多的人一起探讨吧!