1

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?

3
  • You already asked the exactly same question some 10 hours ago, didn't you? Commented Jul 15, 2021 at 22:30
  • No, I did not…. Commented Jul 16, 2021 at 18:31
  • 1
    Okay. I'm sorry about the accusation. Commented Jul 16, 2021 at 21:41

2 Answers 2

2

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")
    }
}
Sign up to request clarification or add additional context in comments.

Comments

0

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

}

2 Comments

I think I was a bit unspecific in my initial post and updated it accordingly: Some values I want in my Device model are not part of the "API Device" model but for example in the System one. While the API response is structured and nested, my model only contains important information in a flat representation.
Then, instead of making it init(withCodableDevice codableDevice: Welcome.Payload.Device), do init(withPayload payload: Welcome.Payload), and self.firmware = payload.device.firmware; self.uptime = payload.system.uptime...

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.