2

We have some response returned by backend:

{
    "name": "Some name",
    "number": 42,
    ............
    "param0": value0,
    "param1": value1,
    "param2": value2
}

Model structure for response:

struct Model: Codable {
    let name: String
    let number: Int
    let params: [String: Any]
}

How to make JSONDecoder combine all unknown key-value pairs into params property?

iOS User
  • 101
  • 1
  • 8
  • 1
    Are there any limits on the types of `value#`? Can it be any JSON value (i.e. could they be an object or array or null), or are they all "strings or ints" or are they all the same (for example strings)? (This is a totally solvable problem in all cases; it's just simpler the more you can restrict the types of value. It's not possible for `params` to really be `[String: Any]` since JSON cannot encode `Any`. So it would be nice to change that property's type to something more restricted.) – Rob Napier May 01 '18 at 13:32

1 Answers1

2

Decodable is incredibly powerful. It can decode completely arbitrary JSON, so this is just a sub-set of that problem. For a fully worked-out JSON Decodable, see this JSON.

I'll pull the concept of Key from example, but for simplicity I'll assume that values must be either Int or String. You could make parameters be [String: JSON] and use my JSON decoder instead.

struct Model: Decodable {
    let name: String
    let number: Int
    let params: [String: Any]

    // An arbitrary-string Key, with a few "well known and required" keys
    struct Key: CodingKey, Equatable {
        static let name = Key("name")
        static let number = Key("number")

        static let knownKeys = [Key.name, .number]

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

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

        // First decode what we know
        name = try container.decode(String.self, forKey: .name)
        number = try container.decode(Int.self, forKey:. number)

        // Find all the "other" keys
        let optionalKeys = container.allKeys
            .filter { !Key.knownKeys.contains($0) }

        // Walk through the keys and try to decode them in every legal way
        // Throw an error if none of the decodes work. For this simple example
        // I'm assuming it is a String or Int, but this is also solvable for
        // arbitarily complex data (it's just more complicated)
        // This code is uglier than it should be because of the `Any` result.
        // It could be a lot nicer if parameters were a more restricted type
        var p: [String: Any] = [:]
        for key in optionalKeys {
            if let stringValue = try? container.decode(String.self, forKey: key) {
                p[key.stringValue] = stringValue
            } else {
                 p[key.stringValue] = try container.decode(Int.self, forKey: key)
            }
        }
        params = p
    }
}

let json = Data("""
{
    "name": "Some name",
    "number": 42,
    "param0": 1,
    "param1": "2",
    "param2": 3
}
""".utf8)

try JSONDecoder().decode(Model.self, from: json)
// Model(name: "Some name", number: 42, params: ["param0": 1, "param1": "2", "param2": 3])

ADDITIONAL THOUGHTS

I think the comments below are really important and future readers should look them over. I wanted to show how little code duplication is required, and how much of this can be easily extracted and reused, such that no magic or dynamic features are required.

First, extract the pieces that are common and reusable:

func additionalParameters<Key>(from container: KeyedDecodingContainer<Key>,
                               excludingKeys: [Key]) throws -> [String: Any]
    where Key: CodingKey {
        // Find all the "other" keys and convert them to Keys
        let excludingKeyStrings = excludingKeys.map { $0.stringValue }

        let optionalKeys = container.allKeys
            .filter { !excludingKeyStrings.contains($0.stringValue)}

        var p: [String: Any] = [:]
        for key in optionalKeys {
            if let stringValue = try? container.decode(String.self, forKey: key) {
                p[key.stringValue] = stringValue
            } else {
                p[key.stringValue] = try container.decode(Int.self, forKey: key)
            }
        }
        return p
}

struct StringKey: CodingKey {
    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 }
}

Now, the decoder for Model is reduced to this

struct Model: Decodable {
    let name: String
    let number: Int
    let params: [String: Any]

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

        name = try container.decode(String.self, forKey: StringKey("name"))
        number = try container.decode(Int.self, forKey: StringKey("number"))
        params = try additionalParameters(from: container,
                                          excludingKeys: ["name", "number"].map(StringKey.init))
    }
}

It would be nice if there were some magic way to say "please take care of these properties in the default way," but I don't quite know what that would look like frankly. The amount of code here is about the same as for implementing NSCoding, and much less than for implementing against NSJSONSerialization, and is easily handed to swiftgen if it were too tedious (it's basically the code you have to write for init). In exchange, we get full compile-time type checking, so we know it won't crash when we get something unexpected.

There are a few ways to make even the above a bit shorter (and I'm currently thinking about ideas involving KeyPaths to make it even more convenient). The point is that the current tools are very powerful and worth exploring.

Rob Napier
  • 286,113
  • 34
  • 456
  • 610
  • Thank you! Unfortunatly, JSONDecoder does not support universal solution (when we have other required properties instead of name and number). Maybe it will be implemented in future. But you answer is very helpful! – iOS User May 01 '18 at 14:17
  • @AlexBishop I'm not certain what you mean here by "universal." You can add whichever properties are required. There are ways to make this much more generic if you have a specific problem you're trying to solve (the more generic it is, the more complicated it gets, though). – Rob Napier May 01 '18 at 14:42
  • By "universal" I mean possibility to specify main properties and properties for dictionary. It may be done by specifying userInfo in JSONDecoder – iOS User May 01 '18 at 16:02
  • I don't think a userinfo on JSONDecoder is really the right approach. That wouldn't allow for type-safety. Are you suggesting that you could hand in `Model.self` and also a dictionary of keys? What would you expect to happen if the keys didn't match the properties? I suspect you want to ask a different question than the one you asked. – Rob Napier May 01 '18 at 16:25
  • (In particular, it would be helpful to explain what you would want to be "implemented in the future." I suspect Decodable can already do what you want, unless what you want is not type-safe, in which case Swift will likely never allow it.) – Rob Napier May 01 '18 at 16:32
  • I meant that may be many models for different responses. Model1:{name,number,params}, Model2:{id,date,params} and so on. And main problem is to avoid code duplication. – iOS User May 01 '18 at 16:43
  • Almost all of this can be extracted into a helper object to avoid duplicating much code but the piece that changes. It is a shame that Swift can't inherit much of its default Codable implementation to forward to, but if you had significant numbers of these, SwiftGen would make short work of it. Over-clever solutions are a much greater threat to projects than modest code duplication where the needs are actually different (and the parts that aren't different can be factored out today) – Rob Napier May 01 '18 at 19:26