Swift:解码 JSON 响应并将嵌套的 JSON 存储为字符串或 JSON

Swift: Decode JSON response and store nested JSON as String or JSON

根据网络请求给出以下 JSON;如果您想将其解码为与 Codable 一致的 Swift 对象,但又想保留嵌套的 JSON,即键 configuration_payload 的值,怎么可能你做到了吗?

{
    "registration": {
        "id": "0000-0000-0000-0000-000",
        "device_type": "device",
        "state": "provisioning",
        "thing_uuid": 999999999,
        "discovery_timeout": 10,
        "installation_timeout": 90,
        "configuration_payload":
            {
                "title": "Some Title",
                "url": "https://www.someurl.com/",
                "category": "test",
                "views": 9999
            }
      }
}

使用下面的 Swift struct,我希望能够将 configuration_payload 作为 String.

public struct Registration: Codable {
    public enum State: String, Codable {
        case provisioning, provisioned
    }
    
    public let id, deviceType: String
    public let state: State
    public let error: String?
    public let thingUUID: Int?
    public let discoveryTimeout, installationTimeout: Int
    public let configurationPayload: String?
}

据我所知,Swift 中的 JSONDecoderconfiguration_payload 的值视为嵌套的 JSON 并希望将其解码为自己的对象.更让人困惑的是,configuration_payload 并不总是会 return 相同的 JSON 结构,它会有所不同,所以我无法创建 Swift struct我可以期待并在需要时简单地 JSON 再次对其进行编码。我需要能够将该值存储为字符串,以说明 configuration_payload 键下 JSON 的变化。

这里是 configurationPayload 字典,所以你的 Registration 结构如下所示

struct Registration : Codable {

    let configurationPayload : ConfigurationPayload?
    let deviceType : String?
    let discoveryTimeout : Int?
    let id : String?
    let installationTimeout : Int?
    let state : String?
    let thingUuid : Int?

    enum CodingKeys: String, CodingKey {
            case configurationPayload = "configuration_payload"
            case deviceType = "device_type"
            case discoveryTimeout = "discovery_timeout"
            case id = "id"
            case installationTimeout = "installation_timeout"
            case state = "state"
            case thingUuid = "thing_uuid"
    }

    init(from decoder: Decoder) throws {
            let values = try decoder.container(keyedBy: CodingKeys.self)
            configurationPayload = ConfigurationPayload(from: decoder)
            deviceType = try values.decodeIfPresent(String.self, forKey: .deviceType)
            discoveryTimeout = try values.decodeIfPresent(Int.self, forKey: .discoveryTimeout)
            id = try values.decodeIfPresent(String.self, forKey: .id)
            installationTimeout = try values.decodeIfPresent(Int.self, forKey: .installationTimeout)
            state = try values.decodeIfPresent(String.self, forKey: .state)
            thingUuid = try values.decodeIfPresent(Int.self, forKey: .thingUuid)
    }

}

你的ConfigurationPayload 看起来像这样

struct ConfigurationPayload : Codable {

        let category : String?
        let title : String?
        let url : String?
        let views : Int?

        enum CodingKeys: String, CodingKey {
                case category = "category"
                case title = "title"
                case url = "url"
                case views = "views"
        }
    
        init(from decoder: Decoder) throws {
                let values = try decoder.container(keyedBy: CodingKeys.self)
                category = try values.decodeIfPresent(String.self, forKey: .category)
                title = try values.decodeIfPresent(String.self, forKey: .title)
                url = try values.decodeIfPresent(String.self, forKey: .url)
                views = try values.decodeIfPresent(Int.self, forKey: .views)
        }

}

一种(比您可能想要的更有限)方法是确保 configuration_payload JSON 中的 Value 部分是已知的 Codable 单一类型(String) 而不是 Any 可以产生多种类型(StringIntDouble 等)。

我试图让它与 [String: Any] 一起用于 configuration_payload,问题是 Any 不符合 Codable

然后我尝试使用 [String: String] 实现 configuration_payload 并且能够像下面那样工作。

public struct Registration: Codable {
    public enum State: String, Codable {
        case provisioning, provisioned
    }
    public let id, deviceType: String
    public let state: State
    public let thingUUID: Int?
    public let discoveryTimeout, installationTimeout: Int
    public let configurationPayload: [String: String]? // NOT [String: Any]?
    
    enum CodingKeys: String, CodingKey {
        case id = "id"
        case deviceType = "device_type"
        case state = "state"
        case thingUUID = "thing_uuid"
        case discoveryTimeout = "discovery_timeout"
        case installationTimeout = "installation_timeout"
        case configurationPayload = "configuration_payload"
    }
    
    public init(from decoder: Decoder) throws {
        let values = try decoder.container(keyedBy: CodingKeys.self)
        
        id = try values.decodeIfPresent(String.self, forKey: .id) ?? ""
        deviceType = try values.decodeIfPresent(String.self, forKey: .deviceType) ?? ""
        
        let stateRaw = try values.decodeIfPresent(String.self, forKey: .state) ?? ""
        state = Registration.State(rawValue: stateRaw) ?? .provisioning
        thingUUID = try values.decodeIfPresent(Int.self, forKey: .thingUUID)
        
        discoveryTimeout = try values.decodeIfPresent(Int.self, forKey: .discoveryTimeout) ?? 0
        installationTimeout = try values.decodeIfPresent(Int.self, forKey: .installationTimeout) ?? 0
        
        configurationPayload = try values.decodeIfPresent([String: String].self, forKey: .configurationPayload)
    }
}

测试

let json = Data("""
{
    "id": "0000-0000-0000-0000-000",
    "device_type": "device",
    "state": "provisioning",
    "thing_uuid": 999999999,
    "discovery_timeout": 10,
    "installation_timeout": 90,
    "configuration_payload": {
        "title": "Some Title",
        "url": "https://www.someurl.com/",
        "category": "test",
        "views": "9999"
    }
}
""".utf8
)

let decoded = try JSONDecoder().decode(Registration.self, from: json)
print(decoded)

let encoded = try JSONEncoder().encode(decoded)
print(String(data: encoded, encoding: .utf8))

您可以使用 AnyCodable.

等第三方库将 JSON 对象解码为 [String: Any]

您的 Registration 结构将如下所示:

public struct Registration: Codable {
    public enum State: String, Codable {
        case provisioning, provisioned
    }
    
    public let id, deviceType: String
    public let state: State
    public let error: String?
    public let thingUUID: Int?
    public let discoveryTimeout, installationTimeout: Int
    public let configurationPayload: [String: AnyCodable]?
}

然后你可以将 [String: AnyCodable] 类型转换为 [String: Any] 甚至 String:

let jsonString = """
{
    "id": "0000-0000-0000-0000-000",
    "device_type": "device",
    "state": "provisioning",
    "thing_uuid": 999999999,
    "discovery_timeout": 10,
    "installation_timeout": 90,
    "configuration_payload":
        {
            "title": "Some Title",
            "url": "https://www.someurl.com/",
            "category": "test",
            "views": 9999
        }
}
"""

let decoder = JSONDecoder()
decoder.keyDecodingStrategy = .convertFromSnakeCase
do {
    let registration = try decoder.decode(Registration.self, from: Data(jsonString.utf8))
    
    // to [String: Any]
    let dictionary = registration.configurationPayload?.mapValues { [=11=].value }

    // to String
    if let configurationPayload = registration.configurationPayload {
        let encoder = JSONEncoder()
        encoder.keyEncodingStrategy = .convertToSnakeCase
        let data = try encoder.encode(configurationPayload)
        let string = String(decoding: data, as: UTF8.self)
        print(string)
    }
} catch {
    print(error)
}

这对于 Codable 协议是不可能的,因为你事先不知道类型。您必须编写自己的方法或采用不同的解码策略。

let json = """
            {
                 "id": "0000-0000-0000-0000-000",
                 "device_type": "device",
                 "state": "provisioning",
                 "thing_uuid": 999999999,
                 "discovery_timeout": 10,
                 "installation_timeout": 90,
                 "configuration_payload": {
                       "title": "Some Title",
                       "url": "https://www.someurl.com/",
                       "category": "test",
                       "views": 9999
                       }
                  }
            
            """.data(using: .utf8)
            
            do {
                let decoded = try? Registration.init(jsonData: json!)
                print(decoded)
            }catch {
                print(error)
            }


public struct Registration {
    public enum State: String, Codable {
        case provisioning, provisioned
    }
    
    public let id: String
    public let device_type: String
    public let state: State
    public let error: String?
    public let thing_uuid: Int?
    public let discovery_timeout, installation_timeout: Int
    public let configuration_payload: [String: Any]?

    public init(jsonData: Data) throws {
        
        let package = try JSONSerialization.jsonObject(with: jsonData, options: []) as! [String : Any]
        
        id = package["id"] as! String
        device_type = package["device_type"] as! String
        state = State(rawValue: package["state"] as! String)!
        error = package["error"] as? String
        thing_uuid = package["thing_uuid"] as? Int
        discovery_timeout = package["discovery_timeout"] as! Int
        installation_timeout = package["installation_timeout"] as! Int
        configuration_payload = package["configuration_payload"] as? [String: Any]
    }
}

这是处理不同类型的一种可能方法。您还可以创建一个包含键的结构并循环遍历它们,我认为这说明了基本思想。

编辑:

 if let remaining = package["configuration_payload"] as? Data,
            let data = try? JSONSerialization.data(withJSONObject: remaining, options: []) as Data,
            let string = String(data: data, encoding: .utf8) {
            // store your string if you want it in string formatt
            print(string)
        }

如果你有一个可能的键列表,使用可选项是你可以使用 Codable 的另一种方式。您可以通过这种方式混合密钥 - 只有可用的密钥才会尝试成为 encoded/decoded

import UIKit

public struct Registration: Codable {
    public enum State: String, Codable {
        case provisioning, provisioned
    }
    
    public let id, deviceType: String
    public let state: State
    public let error: String?
    public let thingUuid: Int?
    public let discoveryTimeout, installationTimeout: Int
    public var configurationPayload: ConfigurationPayload?
}

// nested json can be represented as a codable struct
public struct ConfigurationPayload: Codable {
    
    let title: String?
    let url: String?
    let category: String?
    let views: Int?
    let nonTitle: String?
    let anotherUrl: String?
    let someCategory: String?
    let someViews: Int?
    // computed properties aren't part of the coding strategy
    // TODO: avoid duplication in loop
    var jsonString: String {
        
        let mirror = Mirror(reflecting: self).children
        let parameters = mirror.compactMap({[=10=].label})
        let values = mirror.map({[=10=].value})
        
        let keyValueDict = zip(parameters, values)

        var returnString: String = "{\n"        
        for (key, value) in keyValueDict {
            if let value = value as? Int {
                returnString.append("\"\(key)\": \"\(value)\n")
            } else if let value = value as? String {
                returnString.append("\"\(key)\": \"\(value)\n")
            }
            
        }
        returnString.append("}")
    
        return returnString
    }
}

// your json has a preceding key of "registration", this is the type you will decode
public struct RegistrationParent: Codable {
    var registration: Registration
}

let jsonDataA =
"""
{
    "registration": {
        "id": "0000-0000-0000-0000-000",
        "device_type": "device",
        "state": "provisioning",
        "thing_uuid": 999999999,
        "discovery_timeout": 10,
        "installation_timeout": 90,
        "configuration_payload":
            {
                "title": "Some Title",
                "url": "https://www.someurl.com/",
                "category": "test",
                "views": 9999
            }
      }
}
""".data(using: .utf8)!

let jsonDataB =
"""
{
    "registration": {
        "id": "0000-0000-0000-0000-000",
        "device_type": "device",
        "state": "provisioning",
        "thing_uuid": 999999999,
        "discovery_timeout": 10,
        "installation_timeout": 90,
        "configuration_payload":
            {
                "non_title": "Some Title",
                "another_url": "https://www.someurl.com/",
                "some_category": "test",
                "some_views": 9999
            }
      }
}
""".data(using: .utf8)!


let decoder = JSONDecoder()
decoder.keyDecodingStrategy = .convertFromSnakeCase

do {
    var registrationA = try decoder.decode(RegistrationParent.self, from: jsonDataA)
    print(registrationA.registration.configurationPayload?.jsonString ?? "{}")
    var registrationB = try decoder.decode(RegistrationParent.self, from: jsonDataB)
    print(registrationB.registration.configurationPayload?.jsonString ?? "{}")
} catch {
    print(error)
}

正如其他人所说,你不能只保留一部分而不解码。然而,解码未知数据是微不足道的:

enum RawJsonValue {
    case boolean(Bool)
    case number(Double)
    case string(String)
    case array([RawJsonValue?])
    case object([String: RawJsonValue])
}

extension RawJsonValue: Codable {
    init(from decoder: Decoder) throws {
        let container = try decoder.singleValueContainer()

        if let boolValue = try? container.decode(Bool.self) {
            self = .boolean(boolValue)
        } else if let numberValue = try? container.decode(Double.self) {
            self = .number(numberValue)
        } else if let stringValue = try? container.decode(String.self) {
            self = .string(stringValue)
        } else if let arrayValue = try? container.decode([RawJsonValue?].self) {
            self = .array(arrayValue)
        } else {
            let objectValue = try container.decode([String: RawJsonValue].self)
            self = .object(objectValue)
        }
    }

    func encode(to encoder: Encoder) throws {
        var container = encoder.singleValueContainer()

        switch self {
        case .boolean(let boolValue):
            try container.encode(boolValue)
        case .number(let numberValue):
            try container.encode(numberValue)
        case .string(let stringValue):
            try container.encode(stringValue)
        case .array(let arrayValue):
            try container.encode(arrayValue)
        case .object(let objectValue):
            try container.encode(objectValue)
        }
    }
}

现在我们可以安全地解码并根据需要转换为 JSON 字符串:

struct Registration: Codable {
    public enum State: String, Codable {
        case provisioning, provisioned
    }

    let id, deviceType: String
    let state: State
    let error: String?
    let thingUUID: Int?
    let discoveryTimeout, installationTimeout: Int
    let configurationPayload: RawJsonValue?
}

let jsonData = """
{
    "id": "0000-0000-0000-0000-000",
    "device_type": "device",
    "state": "provisioning",
    "thing_uuid": 999999999,
    "discovery_timeout": 10,
    "installation_timeout": 90,
    "configuration_payload":
        {
            "title": "Some Title",
            "url": "https://www.someurl.com/",
            "category": "test",
            "views": 9999
        }
}
""".data(using: .utf8)!

let decoder = JSONDecoder()
decoder.keyDecodingStrategy = .convertFromSnakeCase
let registration = try! decoder.decode(Registration.self, from: jsonData)

let encoder = JSONEncoder()
encoder.keyEncodingStrategy = .convertToSnakeCase

let payloadString = String(data: try! encoder.encode(registration.configurationPayload), encoding: .utf8)!
print(payloadString) // {"title":"Some Title","views":9999,"url":"https:\/\/www.someurl.com\/","category":"test"}

我能看到的唯一问题是解码十进制数时可能会丢失精度,这是 Foundation JSON 解码器的一个已知问题。 此外,还可以删除一些 null 值。这可以通过迭代键并具有特殊的 null 类型手动解码 object 来解决。