Reputation: 1763
I have the following JSON:
{
"header":{
"namespace":"Device",
"name":"Response",
"messageID":"60FA815A-DC432316",
"payloadVersion":"1"
},
"payload":{
"device":{
"firmware":"1.23W",
"name":"Device 1",
"uuid":"0ba64a0c-7a88b278-0001",
"security":{
"code":"aXdAPqd2OO9sZ6evLKjo2Q=="
}
},
"system":{
"uptime":5680126
}
}
}
I created the Swift structs using quicktype.io:
// MARK: - Welcome
struct Welcome: Codable {
let header: Header
let payload: Payload
}
// MARK: - Header
struct Header: Codable {
let namespace, name, messageID, payloadVersion: String
}
// MARK: - Payload
struct Payload: Codable {
let device: Device
let system: System
}
// MARK: - Device
struct Device: Codable {
let firmware, name, uuid: String
let security: Security
}
// MARK: - Security
struct Security: Codable {
let code: String
}
// MARK: - System
struct System: Codable {
let uptime: Int
}
However, I already have a Device
type, that is a bit more minimal:
struct Device: Identifiable {
let id: UUID
let ip: String
let name: String
let firmware: String
let uptime: Double
// ...
}
How can I nicely decode the raw JSON data into my Device
struct? Note that my Device
is flat and has fields, that are more deeply nested in the original API response model. Do I a custom Decodable
implementation?
Upvotes: 1
Views: 97
Reputation: 299275
You can create intermediate CodingKeys, but this often gets pretty tedious and unnecessary. Instead you can make a general-purpose "string-key" like:
struct AnyStringKey: CodingKey, Hashable, ExpressibleByStringLiteral {
var stringValue: String
init(stringValue: String) { self.stringValue = stringValue }
init<S: StringProtocol>(_ stringValue: S) { self.init(stringValue: String(stringValue)) }
var intValue: Int?
init?(intValue: Int) { return nil }
init(stringLiteral value: String) { self.init(value) }
}
With that, you can navigate your structure pretty easily in a single decoder init by decoding nested containers:
extension Device: Decodable {
init(from decoder: Decoder) throws {
let root = try decoder.container(keyedBy: AnyStringKey.self)
let header = try root.nestedContainer(keyedBy: AnyStringKey.self, forKey: "header")
self.name = try header.decode(String.self, forKey: "name")
let payload = try root.nestedContainer(keyedBy: AnyStringKey.self, forKey: "payload")
let device = try payload.nestedContainer(keyedBy: AnyStringKey.self, forKey: "device")
self.id = try device.decode(UUID.self, forKey: "uuid")
self.firmware = try device.decode(String.self, forKey: "firmware")
let system = try payload.nestedContainer(keyedBy: AnyStringKey.self, forKey: "system")
self.uptime = try system.decode(Double.self, forKey: "uptime")
}
}
(I skipped ip
because it's not in your data, and I assumed that your UUID was just a typo since it's not valid.)
With this, you should be able to decode any part you need.
This is very straightforward and standard, but if you have a lot of things to decode it can get a little tedious. You can improve it with a helper function in that case.
extension KeyedDecodingContainer {
func decode<T>(_ type: T.Type, forPath path: String) throws -> T
where T : Decodable, Key == AnyStringKey {
let components = path.split(separator: ".")
guard !components.isEmpty else {
throw DecodingError.keyNotFound(AnyStringKey(path),
.init(codingPath: codingPath,
debugDescription: "Could not find path \(path)",
underlyingError: nil))
}
if components.count == 1 {
return try decode(type, forKey: AnyStringKey(components[0]))
} else {
let container = try nestedContainer(keyedBy: AnyStringKey.self, forKey: AnyStringKey(components[0]))
return try container.decode(type, forPath: components.dropFirst().joined(separator: "."))
}
}
}
With this, you can access values by a dotted-path syntax:
extension Device: Decodable {
init(from decoder: Decoder) throws {
let root = try decoder.container(keyedBy: AnyStringKey.self)
self.name = try root.decode(String.self, forPath: "header.name")
self.id = try root.decode(UUID.self, forPath: "payload.device.uuid")
self.firmware = try root.decode(String.self, forPath: "payload.device.firmware")
self.uptime = try root.decode(Double.self, forPath: "payload.system.uptime")
}
}
Upvotes: 2
Reputation: 26026
I see two quick possible solutions:
Rename the Codable Device:
struct Device: Codable {
...
}
into
struct DeviceFromAPI: Codable {
...
}
And then replace
struct Payload: Codable {
let device: Device
...
}
into
struct Payload: Codable {
let device: DeviceFromAPI
...
}
Use nested structures.
Put everything inside Welcome
(which is the default QuickType.io name by the way, might be interesting to rename it).
struct Welcome: Codable {
let header: Header
let payload: Payload
// MARK: - Header
struct Header: Codable {
let namespace, name, messageID, payloadVersion: String
}
...
}
Go even if needed to put Device
in Payload
.
Then, you just have to use Welcome.Payload.Device
or Welcome.Device
(depending on how you nested it) when you want to refer to your Codable Device, and just Device
when it's your own.
Then, just have a custom init()
for Device
with the Codable Device
as a parameter.
extension Device {
init(withCodableDevice codableDevice: DeviceFromAPI) {
self.firmware = codableDevice.firmware
...
}
}
or with solution 2:
extension Device {
init(withCodableDevice codableDevice: Welcome.Payload.Device) {
self.firmware = codableDevice.firmware
...
}
}
Upvotes: 0