解码具有动态密钥的 JSON

Decode JSON that has dynamic key

我的回复如下所示 JSON。问题是我有散列的动态密钥。我为这个响应创建一个 Decodable 结构时很迷茫。

我试过下面的代码,但由于最近引入了 planName,所以它因数据不匹配而失败。

struct ProductDescriptionResponse {
    let disclaimersHtml: String?
    let additionalProperties: [String: ProductDescription]?
}

struct ProductDescription {
    var disclaimersHtml: String?
    var hasPlanDetailsV2: Bool?
    let planDescription: String
    let serviceDescriptions: [ServiceDescriptions]
}

struct ServiceDescriptions {
    let name: String
    let subTitle: String?
    let icon: String?
    let description: String?
    let upgradeText: String?
    let featureDisclaimer: String?
}

func productDescriptions() -> Observable<ProductDescriptionResponse> {

        return APIClient.sharedInstance.rx.response(apiURL: APIURL.productDescription, requestType: .get, httpBody: nil, header: .auth).map({ (responseData) -> ProductDescriptionResponse in

            var parsedData = try JSONSerialization.jsonObject(with: responseData) as? [String:Any]

            // Remove disclaimersHtml key from responseData & hold it to pass
            // in ProductDescriptionResponse constructor during return.
            let disclaimersHtml = parsedData?.removeValue(forKey: "disclaimersHtml") as? String
            // Product descriptions.
            var productDescriptions: [String: ProductDescription]? = nil

            if let parsedData = parsedData {

                // Json data without disclaimersHtml.
                let jsonData = try JSONSerialization.data(withJSONObject: parsedData, options: .prettyPrinted)

                productDescriptions =  try JSONDecoder().decode([String: ProductDescription].self, from: jsonData)
            }

            // ProductDescriptionResponse with disclaimersHtml & productDescriptions.
            return ProductDescriptionResponse(disclaimersHtml: disclaimersHtml,
                                              additionalProperties: productDescriptions)
        })
    }

JSON 响应:

{
    "disclaimersHtml": "",
    "planName": "",
    “abc456753234”: {
        "planDescription": "",
        "hasPlanDetailsV2": false,
        "serviceDescriptions": [
            {
                "name": "",
                "subTitle": "",
                "icon": "",
                "hasTile": "",
                "tileTitle": "",
                "description": "",
                "featureDisclaimer": "",
                "upgradeText": ""
            },
            {
                "name": "",
                "subTitle": "",
                "icon": "",
                "hasTile": "",
                "tileTitle": "",
                "description": "",
                "featureDisclaimer": "",
                "upgradeText": ""
            }
        ]
    },
    “xyz123456789”: {
        "planDescription": "",
        "hasPlanDetailsV2": true,
        "serviceDescriptions": [
            {
                "name": "",
                "subTitle": "",
                "icon": "",
                "hasTile": "",
                "tileTitle": "",
                "description": "",
                "featureDisclaimer": "",
                "upgradeText": ""
            }
        ]
    }
}

如果我在下面这样做,它就会起作用。但是不想像这样继续硬编码:

let _ = parsedData?.removeValue(forKey: "planName") as? String

这种JSON有没有更好的方法来工作?我不想硬编码并继续剥离值以获得 ProductDescriptionResponse 因为这个字段可以在将来添加。

我解决的方法是迭代序列化JSON的(key, value)

检查 valueDictionary<AnyHashable,Any> 类型并仅在匹配时解码,否则忽略。

func productDescriptions() -> Observable<ProductDescriptionResponse> {

        return APIClient.sharedInstance.rx.response(memberAPIURL: MemberAPIURL.productDescription, requestType: .get, httpBody: nil, header: .auth).map({ (responseData) -> ProductDescriptionResponse in

            var productDescriptionResponse = ProductDescriptionResponse(disclaimersHtml: nil, additionalProperties: nil)

            var additionalParams: [String: ProductDescription] = [:]

                do {

                    productDescriptionResponse  =  try JSONDecoder().decode(ProductDescriptionResponse.self, from: responseData)

                    if let jsonObject = try JSONSerialization.jsonObject(with: responseData, options: .mutableLeaves) as? [String : Any] {

                        for (key,value) in jsonObject {
                            if value is Dictionary<AnyHashable,Any> {
                                let jsonData = try JSONSerialization.data(withJSONObject: value, options: JSONSerialization.WritingOptions.prettyPrinted)
                                let productDescription =  try JSONDecoder().decode(ProductDescription.self, from: jsonData)
                                additionalParams[key] = productDescription
                            }
                        }
                        productDescriptionResponse.additionalProperties = additionalParams

                    }

                } catch {
                    // handle error
                }

            return productDescriptionResponse
        })
    }

我会使用 CustomCodingKeys 解决这个问题:

struct ProductDescriptionResponse: Decodable {

    let disclaimersHtml: String?
    let additionalProperties: [String: ProductDescription]?
    var disclaimersHtml: String? = nil
    var additionalProperties: [String: ProductDescription]? = nil

    private struct CustomCodingKeys: CodingKey {
        var stringValue: String

        init?(stringValue: String) {
            self.stringValue = stringValue
        }
        var intValue: Int?
        init?(intValue: Int) {
            return nil
        }
    }

    public init(from decoder: Decoder) throws {
        let container = try decoder.container(keyedBy: CustomCodingKeys.self)
        self.additionalProperties = [String: ProductDescription]()
        for key in container.allKeys {
            do {
                if let keyValue = CustomCodingKeys(stringValue: key.stringValue) {
                    if  keyValue.stringValue == "disclaimersHtml" {
                        self.disclaimersHtml = try container.decodeIfPresent(String.self, forKey: keyValue)
                    }

                    self.additionalProperties?[keyValue.stringValue] = try container.decodeIfPresent(ProductDescription.self, forKey: keyValue)
                }
            } catch {
                // Silently ignore the error
            }
        }
    }
}

 func productDescriptions() -> Observable<ProductDescriptionResponse> {

        return APIClient.sharedInstance.rx.response(memberAPIURL: MemberAPIURL.productDescription, requestType: .get, httpBody: nil, header: .auth).map({ (response) -> ProductDescriptionResponse in
            return try JSONDecoder().decode(ProductDescriptionResponse.self, from: response)

        })
    }