可以将文章内容翻译成中文,广告屏蔽插件可能会导致该功能失效(如失效,请关闭广告屏蔽插件后再试):
问题:
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)
}
}