4

I have the following JSON

{"DynamicKey":6410,"Meta":{"name":"","page":""}}

DynamicKey is unknown at compile time.I'm trying to find a reference how to parse this struct using decodable.

public struct MyStruct: Decodable {
    public let unknown: Double
    public let meta: [String: String]

    private enum CodingKeys: String, CodingKey {
        case meta = "Meta"
    }
}

Any ideas?

rmaddy
  • 314,917
  • 42
  • 532
  • 579
sger
  • 719
  • 2
  • 12
  • 26

2 Answers2

8

To decode an arbitrary string, you need a key like this:

// Arbitrary key
private struct Key: CodingKey, Hashable, CustomStringConvertible {
    static let meta = Key(stringValue: "Meta")!

    var description: String {
        return stringValue
    }

    var hashValue: Int { return stringValue.hash }

    static func ==(lhs: Key, rhs: Key) -> Bool {
        return lhs.stringValue == rhs.stringValue
    }

    let stringValue: String
    init(_ string: String) { self.stringValue = string }
    init?(stringValue: String) { self.init(stringValue) }
    var intValue: Int? { return nil }
    init?(intValue: Int) { return nil }
}

This is a very general-purpose tool (expect for the static let meta) that can be used for all kinds of generic-key problems.

With that, you can find the first key that isn't .meta and use that as your dynamic key.

public init(from decoder: Decoder) throws {
    let container = try decoder.container(keyedBy: Key.self)

    meta = try container.decode([String: String].self, forKey: .meta)

    guard let dynamicKey = container.allKeys.first(where: { $0 != .meta }) else {
        throw DecodingError.dataCorrupted(DecodingError.Context(codingPath: [],
                                                                debugDescription: "Could not find dynamic key"))
    }

    unknown = try container.decode(Double.self, forKey: dynamicKey)
}

All together as a playground:

import Foundation

let json = Data("""
{"DynamicKey":6410,"Meta":{"name":"","page":""}}
""".utf8)

public struct MyStruct: Decodable {
    public let unknown: Double
    public let meta: [String: String]

    // Arbitrary key
    private struct Key: CodingKey, Hashable, CustomStringConvertible {
        static let meta = Key(stringValue: "Meta")!
        var description: String {
            return stringValue
        }

        var hashValue: Int { return stringValue.hash }

        static func ==(lhs: Key, rhs: Key) -> Bool {
            return lhs.stringValue == rhs.stringValue
        }

        let stringValue: String
        init(_ string: String) { self.stringValue = string }
        init?(stringValue: String) { self.init(stringValue) }
        var intValue: Int? { return nil }
        init?(intValue: Int) { return nil }
    }

    public init(from decoder: Decoder) throws {
        let container = try decoder.container(keyedBy: Key.self)

        meta = try container.decode([String: String].self, forKey: .meta)

        guard let dynamicKey = container.allKeys.first(where: { $0 != .meta }) else {
            throw DecodingError.dataCorrupted(.init(codingPath: [],
                                                    debugDescription: "Could not find dynamic key"))
        }

        unknown = try container.decode(Double.self, forKey: dynamicKey)
    }
}


let myStruct = try! JSONDecoder().decode(MyStruct.self, from: json)
myStruct.unknown
myStruct.meta

This technique can be expanded to decode arbitrary JSON. Sometimes it's easier to do that, and then pull out the pieces you want, then to decode each piece. For example, with the JSON gist above, you could implement MyStruct this way:

public struct MyStruct: Decodable {
    public let unknown: Double
    public let meta: [String: String]

    public init(from decoder: Decoder) throws {
        let container = try decoder.singleValueContainer()
        let json = try container.decode(JSON.self)

        guard let meta = json["Meta"]?.dictionaryValue as? [String: String] else {
            throw DecodingError.dataCorrupted(.init(codingPath: [],
                                                    debugDescription: "Could not find meta key"))
        }
        self.meta = meta

        guard let (_, unknownJSON) = json.objectValue?.first(where: { (key, _) in key != "Meta" }),
            let unknown = unknownJSON.doubleValue
        else {
            throw DecodingError.dataCorrupted(.init(codingPath: [],
                                                    debugDescription: "Could not find dynamic key"))
        }
        self.unknown = unknown
    }
}
Rob Napier
  • 286,113
  • 34
  • 456
  • 610
  • @Rob nice but my implementation seems shorter – Vyacheslav Nov 12 '18 at 21:07
  • @Vyacheslav The extra baggage your removed is fine (`description`, `==`, and `hashValue` are unnecessary; they came over from my larger JSON implementation). But you changed an important part of the problem: you made everything optionals. Yes, that got you out of having to throw errors (which is why your `init` is shorter), but that changes the data structure in really important way. You also made the `let` values `var`, which again makes `init` simpler at the expense of the immutability of the object. – Rob Napier Nov 12 '18 at 21:32
  • 1
    The choice choice to use "the first thing that decodes as Double" vs "the first key that isn't Meta" is probably fine; neither way is really checking the data very closely. But it's important to know which validation you're using. For example, if another mandatory `Double` field were added, your approach would become fragile. If another optional field (one we didn't parse) were added to the JSON, then mine would break. – Rob Napier Nov 12 '18 at 21:36
  • @RobNapier Hi, do you think i can apply your answers to accomplish what i am asking here or i should use enum associated type or some sort of combination? https://stackoverflow.com/questions/66283127/swift-codable-reusing-a-subset-of-keys-shared-across-different-models-struct-cla?noredirect=1#comment117187097_66283127 . Thank you for the time – Coder Feb 20 '21 at 11:46
2
import UIKit

var str = """
{"DynamicKey":6410,"Meta":{"name":"","page":""}}
"""
public struct MyStruct: Decodable {
    public var unknown: Double?
    public var meta: [String: String]?

    public init(from decoder: Decoder) {

        guard let container = try? decoder.container(keyedBy: CodingKeys.self) else {
            fatalError()
        }
            for key in container.allKeys {
                unknown = try? container.decode(Double.self, forKey: key)//) ?? 0.0
                if key.stringValue == "Meta" {
                    meta = try? container.decode([String: String].self, forKey: key)
                }

            }
            print(container.allKeys)
    }

    struct CodingKeys: CodingKey {
        var stringValue: String
        init?(stringValue: String) {
            self.stringValue = stringValue
        }
        var intValue: Int?
        init?(intValue: Int) {
            return nil
        }
    }
}
    let jsonData = str.data(using: .utf8)!
    let jsonDecoder = JSONDecoder()
    let myStruct = try! jsonDecoder.decode(MyStruct.self, from: jsonData)
    print("Meta : \(myStruct.meta)")
    print("Double : \(myStruct.unknown)")

I've already answered a similar question

https://stackoverflow.com/a/48412139/1979882

Vyacheslav
  • 26,359
  • 19
  • 112
  • 194