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