使用 Swift for iOS 自定义嵌套对象的 JSON 解码器

Customise JSON decoder for nested object with Swift for iOS

提问人:romainb78 提问时间:8/28/2023 最后编辑:romainb78 更新时间:8/29/2023 访问量:72

问:

使用 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内容取决于 。所以嵌套对象需要知道才能解码。typecontenttypeattributes

结构:

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

如何将解码传递给嵌套对象进行解码?typeResponseContentResponse

不工作 #1

我在这里找到了一个 https://www.andyibanez.com/posts/the-mysterious-codablewithconfiguration-protocol/ 使用的解决方案,但它针对的是 iOS 15+,而我的目标是 iOS 13+。CodableWithConfigurationdecodeIfPresent(_:forKey:configuration:)

不工作 #2

我本可以使用 in 传递 ,但它是只读的:userInfodecoderinit(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)
}

iOS JSON Swift 解码 解码器

评论


答:

3赞 Joakim Danielson 8/28/2023 #1

为此,我将使用枚举,一个用于内容类型,一个用于保存解码的内容。

首先是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
}

然后,所有自定义解码都参与使用嵌套容器对值进行解码的位置。ResponseContent

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
}

评论

0赞 Rob 8/28/2023
好答案(+1)。我唯一可以添加的是更改,以便在解码值不在其枚举值中时不会引发错误。我还捕获了未知字符串的值(我发现当服务器引入某些未知键时,这有助于诊断问题)。例如,gist.github.com/robertmryan/696c7409de2da38ee568f53e16154674ContentType
1赞 Joakim Danielson 8/28/2023
谢谢,这是一个很好的观点。我将添加我的解决方案,该解决方案略有不同,但基于您建议的想法。