LuMa
LuMa

Reputation: 1763

Decode JSON into a target struct which differs from the JSON model

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

Answers (2)

Rob Napier
Rob Napier

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

Larme
Larme

Reputation: 26026

I see two quick possible solutions:

Solution 1:

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

Solution2:

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

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

Related Questions