简体   繁体   中英

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

Given the following JSON from a network request; If you wanted to decode this into a Swift object that coforms to Codable , but you wanted to retain the nested JSON that is the value for the key configuration_payload , how could you do it?

{
    "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
            }
      }
}

Using the following Swift struct , I want to be able to grab the configuration_payload as a 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?
}

As far as I can tell, the JSONDecoder in Swift, sees the value for configuration_payload as nested JSON and wants to decode it into it's own object. To add to confusion, configuration_payload is not always going to return the same JSON structure, it will vary, so I can not create a Swift struct that I can expect and simply JSON encode it again when needed. I need to be able to store the value as a String to account for variations in the JSON under the configuration_payload key.

You can achieve decoding of a JSON object to [String: Any] by using a third party library like AnyCodable .

Your Registration struct will look like this:

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]?
}

and then you can convert [String: AnyCodable] type to [String: Any] or even to 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 { $0.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)
}

One (more limited than you probably want) way would be to make sure that Value part in configuration_payload JSON is a known Codable single type ( String ) instead of Any which can produce multiple types ( String , Int , Double etc.).

I was trying to make it work with [String: Any] for the configuration_payload , the problem is Any does NOT conform to Codable .

Then I tried with [String: String] for configuration_payload and was able to make it work like following.

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

Tests

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

This is not possible with the Codable protocol, because you do not know the type before hand. You'll have to either write your own method or have a different decoding strategy.

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]
    }
}

This is one possible way to handle the different types. You could also create a struct containing keys and loop through them, I think this illustrates the basic idea though.

Edit:

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

If you have a list of possible keys, using optionals is another way you could employ Codable. You can mix keys this way - only the ones that are available will attempt to be 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({$0.label})
        let values = mirror.map({$0.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)
}

在此处输入图片说明

As others have already said, you cannot just keep a part without decoding. However, decoding unknown data is trivial:

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

Now we can safely decode and convert to JSON string if needed:

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"}

The only problem I can see is potential loss of precision when decoding decimal numbers, which is a known problem with Foundation JSON decoder. Also, some null values could be also removed. This could be fixed by decoding object manually by iterating keys and having a special null type.

here is configurationPayload is dictionary so your Registration struct look like below

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

}

and your ConfigurationPayload look like this

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

}

The structure model is impossible to achieve. But we have other ways to save the country by curve:

Solution 1. By storing the json string, take it out and parse it into a model when needed.

Solution 2. Through NSObject, use dynamic runtime to add NSCoding.

    public struct Registration: NSObject, NSCoding {
        
        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?
        
                
        // MARK: -NSCoding
        required init?(coder aDecoder: NSCoder) {
            super.init()
            se_decode(with: aDecoder)
        }
        
        func encode(with aCoder: NSCoder) {
            se_encode(with: aCoder)
        }
        
    }


    @objc public extension NSObject{

        ///enumerate Propertys
        func enumeratePropertys(_ block: @escaping ((objc_property_t, String, Any?)->Void)) {
            var count: UInt32 = 0
            guard let list = class_copyPropertyList(self.classForCoder, &count) else { return }
            defer {
                free(list)
            }
            
            for i in 0..<Int(count) {
                let property: objc_property_t = list[i]
                let pname = property_getName(property)
                guard let name = String(utf8String: pname) else {
                    continue
                }
                let value = self.value(forKey: name)
                block(property, name, value)
            }
        }
    
        /// encode model
        func se_encode(with aCoder: NSCoder) {
            enumeratePropertys { (property, name, value) in
                guard let value = self.value(forKey: name) else { return }
                aCoder.encode(value, forKey: name)
            }
        }
        
        /// decode model
        func se_decode(with aDecoder: NSCoder) {
            enumeratePropertys { (property, name, value) in
                guard let value = aDecoder.decodeObject(forKey: name) else { return }
                self.setValue(value, forKeyPath: name)
            }
        }
    }

The technical post webpages of this site follow the CC BY-SA 4.0 protocol. If you need to reprint, please indicate the site URL or the original address.Any question please contact:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM