提问人:romainb78 提问时间:8/28/2023 最后编辑:romainb78 更新时间:8/29/2023 访问量:72
使用 Swift for iOS 自定义嵌套对象的 JSON 解码器
Customise JSON decoder for nested object with Swift for iOS
问:
使用 iOS 13+ 的 Swift,我需要解码 JSON 响应。它包含一个嵌套对象,该对象需要父对象的值()才能解码。type
JSON 结构:
{
"name":"My email",
"type":"Email",
"content":{
"issued":"2023-08-25T12:58:39Z",
"attributes":{
"email":"[email protected]"
}
}
}
或
{
"name":"My telephone",
"type":"Telephone",
"content":{
"issued":"2023-08-25T12:58:39Z",
"attributes":{
"telephone":"+33123456789"
}
}
}
attributes
内容取决于 。所以嵌套对象需要知道才能解码。type
content
type
attributes
结构:
struct Response: Decodable {
let name: String
let type: String
let content: ContentResponse?
init(from decoder: Decoder) throws {
let container = try decoder.container(keyedBy: CodingKeys.self)
name = try container.decode(String.self, forKey: .name)
type = try container.decode(String.self, forKey: .type)
// !!! Here I need to pass the "type" value to content decoding
content = try container.decodeIfPresent(ContentResponse.self, forKey: .content)
}
}
struct ContentResponse: Decodable {
let issued: Date
let attributes: AttributesResponse?
init(from decoder: Decoder) throws {
let container = try decoder.container(keyedBy: CodingKeys.self)
issued = try container.decode(Date.self, forKey: .issued)
if container.contains(.attributes) {
// !!! Here I can't access the "type" value from parent object
switch Type.fromString(type: type) {
case .telephone:
attributes = try container.decode(AttributesTelephoneResponse.self, forKey: .attributes)
case .email:
attributes = try container.decode(AttributesEmailResponse.self, forKey: .attributes)
default:
// Unsupported type
throw DecodingError.dataCorruptedError(forKey: .attributes, in: contentContainer, debugDescription: "Type \"\(type)\" not supported for attributes")
}
} else {
attributes = nil
}
}
}
class DocumentResponse: Decodable {}
class AttributesEmailResponse: DocumentResponse {
let email: String
}
class AttributesTelephoneResponse: DocumentResponse {
let telephone: String
}
正如你所看到的,需要知道 才能知道使用哪个类进行解码。init(from decoder: Decoder)
ContentResponse
type
attributes
如何将解码传递给嵌套对象进行解码?type
Response
ContentResponse
不工作 #1
我在这里找到了一个 https://www.andyibanez.com/posts/the-mysterious-codablewithconfiguration-protocol/ 使用的解决方案,但它针对的是 iOS 15+,而我的目标是 iOS 13+。CodableWithConfiguration
decodeIfPresent(_:forKey:configuration:)
不工作 #2
我本可以使用 in 传递 ,但它是只读的:userInfo
decoder
init(from decoder: Decoder)
type
var userInfo: [CodingUserInfoKey : Any] { get }
溶液
感谢 @Joakim Danielson 的回答。
解决方案是在以下方面进行整个解码:Response
struct Response: Decodable {
let name: String
let type: String
let content: ContentResponse?
enum CodingKeys: String, CodingKey {
case name
case type
case content
}
init(from decoder: Decoder) throws {
let container = try decoder.container(keyedBy: CodingKeys.self)
name = try container.decode(String.self, forKey: .name)
type = try container.decode(String.self, forKey: .type)
// Content
if container.contains(.content) {
let issued = try contentContainer.decode(Date.self, forKey: .issued)
let attributes: DocumentResponse?
switch DocumentType.fromString(type: type) {
case .telephone:
let value = try contentContainer.decode(DocumentTelephoneResponse.self, forKey: .attributes)
attributes = .telephone(value)
case .email:
let value = try contentContainer.decode(DocumentEmailResponse.self, forKey: .attributes)
attributes = .email(value)
default:
throw DecodingError.dataCorruptedError(forKey: .attributes, in: contentContainer, debugDescription: "Type \"\(type)\" not supported for attributes")
}
content = ContentResponse(issued: issued, expires: expires, attributes: attributes, controls: controls)
} else {
content = nil
}
}
}
跟:
public enum DocumentType: String, Codable {
case telephone = "Telephone"
case email = "Email"
static public func fromString(type: String) -> Type? {
Type(rawValue: type) ?? nil
}
}
struct ContentResponse: Decodable {
let issued: Date
let attributes: DocumentResponse?
enum CodingKeys: String, CodingKey {
case issued
case attributes
}
}
enum DocumentResponse: Decodable {
case email(DocumentEmailResponse)
case telephone(DocumentTelephoneResponse)
}
答:
为此,我将使用枚举,一个用于内容类型,一个用于保存解码的内容。
首先是type
enum ContentType: String, Codable {
case email = "Email"
case phone = "PhoneNumber"
case none
}
然后是content
enum Content: Codable {
case email(DocumentEmailResponse)
case phone(DocumentPhoneNumberResponse)
case none
}
我将中使用的类型更改为结构Content
struct DocumentEmailResponse: Codable {
let email: String
}
struct DocumentPhoneNumberResponse: Codable {
let phoneNumber: String
}
然后,所有自定义解码都参与使用嵌套容器对值进行解码的位置。Response
Content
struct Response: Codable {
let name: String
let type: ContentType
let content: Content
enum ContentCodingKeys: String, CodingKey {
case attributes
}
init(from decoder: Decoder) throws {
let container = try decoder.container(keyedBy: CodingKeys.self)
name = try container.decode(String.self, forKey: .name)
type = try container.decode(ContentType.self, forKey: .type)
let contentContainer = try container.nestedContainer(keyedBy: ContentCodingKeys.self, forKey: .content)
switch type {
case .email:
let value = try contentContainer.decode(DocumentEmailResponse.self, forKey: .attributes)
content = .email(value)
case .phone:
let value = try contentContainer.decode(DocumentPhoneNumberResponse.self, forKey: .attributes)
content = .phone(value)
default:
content = .none
}
}
}
这两个枚举都包含一个大小写,但根据可选或不可选的内容以及您可能希望删除它们的个人偏好,如果可能的话,我不完全确定哪些值以及何时值可以为 nil。none
正如@Rob在评论中指出的那样,在不使解码过程失败的情况下处理未知类型可能是件好事。以下是执行此操作的替代方法,还可以查看评论中的链接以获取另一种替代方法。
用于解码的新自定义 init 是
init(from decoder: Decoder) throws {
let container = try decoder.container(keyedBy: CodingKeys.self)
name = try container.decode(String.self, forKey: .name)
let contentValue = try container.decode(String.self, forKey: .type)
self.type = ContentType(rawValue: contentValue) ?? .none
let contentContainer = try container.nestedContainer(keyedBy: ContentCodingKeys.self, forKey: .content)
switch type {
case .email:
let value = try contentContainer.decode(DocumentEmailResponse.self, forKey: .attributes)
content = .email(value)
case .phone:
let value = try contentContainer.decode(DocumentPhoneNumberResponse.self, forKey: .attributes)
content = .phone(value)
case .none:
content = .unknownType(contentValue)
}
}
这需要对枚举进行更改Content
enum Content: Codable {
case email(DocumentEmailResponse)
case phone(DocumentPhoneNumberResponse)
case unknownType(String)
//case none <- this might still be relevant if content is optional
}
评论
ContentType
评论