Reputation: 1268
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.
Upvotes: 3
Views: 2952
Reputation: 8327
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)
}
Upvotes: 3
Reputation: 130102
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.
Upvotes: 3
Reputation: 1915
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)
}
Upvotes: 1
Reputation: 758
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)
}
Upvotes: 1
Reputation: 10102
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)
}
}
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))
Upvotes: 1
Reputation: 177
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)
}
}
Upvotes: 0