Swift: hook into Decodable.init() for an unspecified key? - json

I have some JSON I would like to decode with a JSONDecoder. Trouble is, the name of one of the properties is helpfully dynamic when sent from the server.
Like this:
{
"someRandomName": [ [1,2,3], [4,5,6] ],
"staticName": 12345
}
How can I decode this, when the someRandomName is not known at build time? I have been trawling through the www looking for an answer, but still no joy. Can't really get my head around how this Decodable, CodingKey stuff works. Some of the examples are dozens of lines long, and that doesn't seem right!
EDIT I should point out that the key is known at runtime, so perhaps I can pass it in when decoding the object?
Is there any way to hook into one of the protocol methods or properties to enable this decoding? I don't mind if I have to write a bespoke decoder for just this object: all the other JSON is fine and standard.
EDIT
Ok, my understanding has taken me this far:
struct Pair: Decodable {
var pair: [[Double]]
var last: Int
private struct CodingKeys: CodingKey {
var stringValue: String
init?(stringValue: String) {
self.stringValue = stringValue
}
// Use for integer-keyed dictionary
var intValue: Int?
init?(intValue: Int) {
// We are not using this, thus just return nil
return nil
}
}
init(from decoder: Decoder) throws {
// just to stop the compiler moaning
pair = [[]]
last = 0
let container = try decoder.container(keyedBy: CodingKeys.self)
// how do I generate the key for the correspond "pair" property here?
for key in container.allKeys {
last = try container.decode(Int.self, forKey: CodingKeys(stringValue: "last")!)
pair = try container.decode([[Double]].self, forKey: CodingKeys(stringValue: key.stringValue)!)
}
}
}
init() {
let jsonString = """
{
"last": 123456,
"XBTUSD": [ [1.0, 2.0, 3.0], [4.0, 5.0, 6.0] ]
}
"""
let jsonData = Data(jsonString.utf8)
// this gives: "Fatal error: 'try!' expression unexpectedly raised an error: Swift.DecodingError.typeMismatch(Swift.Array<Any>, Swift.DecodingError.Context(codingPath: [CodingKeys(stringValue: "last", intValue: nil)], debugDescription: "Expected to decode Array<Any> but found a number instead.", underlyingError: nil))"
let decodedResult = try! JSONDecoder().decode(Pair.self, from: jsonData)
dump(decodedResult)
}
So I now understand that the CodingKey conformance is generating the keys for the serialized data, not the Swift struct (which kinda makes perfect sense now I think about it).
So how do I now generate the case for pair on the fly, rather than hard-coding it like this? I know it has something to do with the init(from decoder: Decoder) I need to implement, but for the life of me I can't work out how that functions. Please help!
EDIT 2
Ok, I'm so close now. The decoding seems to be working with this:
struct Pair: Decodable {
var pair: [[Double]]
var last: Int
private enum CodingKeys : String, CodingKey {
case last
}
private struct DynamicCodingKeys: CodingKey {
var stringValue: String
init?(stringValue: String) {
self.stringValue = stringValue
}
// Use for integer-keyed dictionary
var intValue: Int?
init?(intValue: Int) {
// We are not using this, thus just return nil
return nil
}
}
init(from decoder: Decoder) throws {
// just to stop the compiler moaning
pair = [[]]
last = 0
let container1 = try decoder.container(keyedBy: CodingKeys.self)
last = try container1.decode(Int.self, forKey: .last)
let container2 = try decoder.container(keyedBy: DynamicCodingKeys.self)
for key in container2.allKeys {
pair = try container2.decode([[Double]].self, forKey: DynamicCodingKeys(stringValue: key.stringValue)!)
}
}
}
This code seems to do its job: examining the last and pair properties in the function itself and it looks good; but I'm getting an error when trying to decode:
init() {
let jsonString = """
{
"last": 123456,
"XBTUSD": [ [1.0, 2.0, 3.0], [4.0, 5.0, 6.0] ]
}
"""
let jsonData = Data(jsonString.utf8)
// Fatal error: 'try!' expression unexpectedly raised an error: Swift.DecodingError.typeMismatch(Swift.Array<Any>, Swift.DecodingError.Context(codingPath: [DynamicCodingKeys(stringValue: "last", intValue: nil)], debugDescription: "Expected to decode Array<Any> but found a number instead."
let decodedResult = try! JSONDecoder().decode(Pair.self, from: jsonData)
dump(decodedResult)
}
I'm so close I can taste it...

If the dynamic key is known at runtime, you can pass it via the userInfo dictionary of the decoder.
First of all create two extensions
extension CodingUserInfoKey {
static let dynamicKey = CodingUserInfoKey(rawValue: "dynamicKey")!
}
extension JSONDecoder {
convenience init(dynamicKey: String) {
self.init()
self.userInfo[.dynamicKey] = dynamicKey
}
}
In the struct implement CodingKeys as struct to be able to create keys on the fly.
struct Pair : Decodable {
let last : Int
let pair : [[Double]]
private struct CodingKeys: CodingKey {
var intValue: Int?
var stringValue: String
init?(stringValue: String) { self.stringValue = stringValue }
init?(intValue: Int) {
self.stringValue = String(intValue)
self.intValue = intValue
}
static let last = CodingKeys(stringValue: "last")!
static func makeKey(name: String) -> CodingKeys {
return CodingKeys(stringValue: name)!
}
}
init(from decoder: Decoder) throws {
let container = try decoder.container(keyedBy: CodingKeys.self)
guard let dynamicKey = decoder.userInfo[.dynamicKey] as? String else {
throw DecodingError.dataCorruptedError(forKey: .makeKey(name: "pair"), in: container, debugDescription: "Dynamic key in userInfo is missing")
}
last = try container.decode(Int.self, forKey: .last)
pair = try container.decode([[Double]].self, forKey: .makeKey(name: dynamicKey))
}
}
Now create the JSONDecoder passing the known dynamic name
let jsonString = """
{
"last": 123456,
"XBTUSD": [ [1.0, 2.0, 3.0], [4.0, 5.0, 6.0] ]
}
"""
do {
let decoder = JSONDecoder(dynamicKey: "XBTUSD")
let result = try decoder.decode(Pair.self, from: Data(jsonString.utf8))
print(result)
} catch {
print(error)
}
Edit:
If the JSON contains always only two keys this is an easier approach:
struct AnyKey: CodingKey {
var stringValue: String
var intValue: Int?
init?(stringValue: String) { self.stringValue = stringValue }
init?(intValue: Int) {
self.stringValue = String(intValue)
self.intValue = intValue
}
}
struct Pair : Decodable {
let last : Int
let pair : [[Double]]
}
let jsonString = """
{
"last": 123456,
"XBTUSD": [ [1.0, 2.0, 3.0], [4.0, 5.0, 6.0] ]
}
"""
do {
let decoder = JSONDecoder()
decoder.keyDecodingStrategy = .custom({ codingPath in
let lastPath = codingPath.last!
if lastPath.stringValue == "last" { return lastPath }
return AnyKey(stringValue: "pair")!
})
let result = try decoder.decode(Pair.self, from: Data(jsonString.utf8))
print(result)
} catch {
print(error)
}

You're looking for JSONSerializer not JSONDecoder I guess, https://developer.apple.com/documentation/foundation/jsonserialization.
Because the key is unpredictable, so better convert to Dictionary. Or you can take a look at this https://swiftsenpai.com/swift/decode-dynamic-keys-json/

I now have some code that actually works!
struct Pair: Decodable {
var pair: [[Double]]
var last: Int
private struct CodingKeys: CodingKey {
var intValue: Int?
var stringValue: String
init?(stringValue: String) { self.stringValue = stringValue }
init?(intValue: Int) {
self.stringValue = String(intValue)
self.intValue = intValue
}
static let last = CodingKeys(stringValue: "last")!
static func makeKey(name: String) -> CodingKeys {
return CodingKeys(stringValue: name)!
}
}
init(from decoder: Decoder) throws {
let container = try decoder.container(keyedBy: CodingKeys.self)
last = try container.decode(Int.self, forKey: .last)
let key = container.allKeys.first(where: { $0.stringValue != "last" } )?.stringValue
pair = try container.decode([[Double]].self, forKey: .makeKey(name: key!))
}
}
init() {
let jsonString = """
{
"last": 123456,
"XBTUSD": [ [1.0, 2.0, 3.0], [4.0, 5.0, 6.0] ]
}
"""
let jsonData = Data(jsonString.utf8)
// Ask JSONDecoder to decode the JSON data as DecodedArray
let decodedResult = try! JSONDecoder().decode(Pair.self, from: jsonData)
dump(decodedResult)
}

Related

Swift decode json when one property name / key is dynamic

Json response from this call https://cdn.jsdelivr.net/gh/fawazahmed0/currency-api#1/latest/currencies/usd/eur.min.json is pretty basic
{
"date": "2022-12-27",
"eur": 0.939751
}
first property is always named "date" and it's always String second is Double but it's name/key is dynamic, it can be "usd", "eur" etc.
I have tried
struct RateResponse: Decodable {
let date: String
let rate: Double
enum CodingKeys: CodingKey {
case date
}
init(from decoder: Decoder) throws {
let container = try decoder.container(keyedBy: CodingKeys.self)
date = try container.decode(String.self, forKey: CodingKeys.date)
let singleValueContainer = try decoder.singleValueContainer()
rate = try singleValueContainer.decode(Double.self)
}
}
got this "Expected to decode Double but found a dictionary instead." I know what error says but not to sure how to solve it.
My suggestion is a custom KeyDecodingStrategy.
First you need a neutral CodingKey struct to replace the currency key.
struct AnyKey: CodingKey {
var stringValue: String
var intValue: Int?
init?(stringValue: String) { self.stringValue = stringValue }
init?(intValue: Int) {
self.stringValue = String(intValue)
self.intValue = intValue
}
}
The RateResponse struct can be reduced to
struct RateResponse: Decodable {
let date: String
let rate: Double
}
The key decoding strategy passes the date key and replaces anything else with rate
let jsonString = """
{
"date": "2022-12-27",
"eur": 0.939751
}
"""
do {
let decoder = JSONDecoder()
decoder.keyDecodingStrategy = .custom({
let value = $0.last!.stringValue
switch value {
case "date": return $0.last!
default: return AnyKey(stringValue: "rate")!
}
})
let result = try decoder.decode(RateResponse.self, from: Data(jsonString.utf8))
print(result)
} catch {
print(error)
}

How to use Swift Codable with a nested JSON structure and unknown keys

I have a nested json structure. I know the top level keys (objects) but those may or may not be present for each fetch. Each key within the objects (and the ones nested inside those) is unknown. They are dynamic.
I have tried for hours to use Codable only to use JSONSerialization instead. Before I completely lose hope, I wanted to see if anyone has a solution for this.
Here is an example of what my JSON looks like:
var jsonString =
"""
{
"someNumbers": {
"22": 6,
"22626": 0
},
"someNestedAny": {
"61": {
"browser": 2
},
"8310": {
"desktop": 2
}
},
"someNestedArray": {
"49": ["Chrome"],
"50": ["Mac OS X"],
"51": ["Mac desktop"],
"52": ["browser"],
"53": ["Chrome"]
}
}
"""
The key value pairs within each top level object (someNumbers, someNestedAny, and someNestedArray are dynamic. The keys/values within those objects are also dynamic...and so on. Each top level object is also optional.
I have tried many things, but these are what have looked the most promising (neither of them worked, tho)
struct TopLevel: Decodable {
var someNumbers: SomeNumbers?
var someNestedAny: SomeNestedAny?
var someNestedArray: SomeObjectFromNestedAny?
}
struct SomeNumbers: Decodable {
var key: String
var value: Int
}
struct SomeNestedAny: Decodable {
var key: String
var value: SomeObjectFromNestedAny
}
struct SomeObjectFromNestedAny: Decodable {
var key: String
var value: Int
}
struct SomeNestedArray: Decodable {
var key: String
var value: [String]
}
let data = jsonString.data(using: .utf8)!
do {
let result = try JSONDecoder().decode(TopLevel.self, from: data)
print(result)
} catch {
print(error)
}
Output: keyNotFound(CodingKeys(stringValue: "key", intValue: nil), Swift.DecodingError.Context(codingPath: [CodingKeys(stringValue: "someNumbers", intValue: nil)], debugDescription: "No value associated with key CodingKeys(stringValue: \"key\", intValue: nil) (\"key\").", underlyingError: nil))
struct TopLevel: Decodable {
var someNumbers: SomeNumbers?
var someNestedAny: SomeNestedAny?
var someNestedArray: SomeObjectFromNestedAny?
}
struct SomeNumbers: Decodable {
public var numbersObject: [String: NumberKeys]
public struct NumberKeys: Decodable {
public let key: String
public let value: Int
}
private struct NumberCodingKeys: CodingKey {
var stringValue: String
init?(stringValue: String) {
self.stringValue = stringValue
}
var intValue: Int?
init?(intValue: Int) {
return nil
}
}
public init(from decoder: Decoder) throws {
let container = try decoder.container(keyedBy: NumberCodingKeys.self)
self.numbersObject = [String: NumberKeys]()
for key in container.allKeys {
let value = try container.decode(NumberKeys.self, forKey: NumberCodingKeys(stringValue: key.stringValue)!)
self.numbersObject[key.stringValue] = value
}
}
}
struct SomeNestedAny: Decodable {
// was going to do the same thing as above - but it didn't work
var key: String
var value: SomeObjectFromNestedAny
}
struct SomeObjectFromNestedAny: Decodable {
// was going to do the same thing as above - but it didn't work
var key: String
var value: Int
}
struct SomeNestedArray: Decodable {
// was going to do the same thing as above - but it didn't work
var key: String
var value: [String]
}
let data = jsonString.data(using: .utf8)!
do {
let result = try JSONDecoder().decode(TopLevel.self, from: data)
print(result)
} catch {
print(error)
}
Output: typeMismatch(Swift.Dictionary<Swift.String, Any>, Swift.DecodingError.Context(codingPath: [CodingKeys(stringValue: "someNumbers", intValue: nil), NumberCodingKeys(stringValue: "22626", intValue: nil)], debugDescription: "Expected to decode Dictionary<String, Any> but found a number instead.", underlyingError: nil))
Here is what I have working now, but is really gross:
let data = jsonString.data(using: .utf8)
let json = try JSONSerialization.jsonObject(with: data!) as! [String:Any]
if let numbers = json["someNumbers"] as? [String:Any] {
for (key, value) in numbers {
print("key: \(key), value: \(value)")
}
}
if let anys = json["someNestedAny"] as? [String: Any] {
//print("tkey: \(anys)")
for (key, value) in anys {
//print("tkey2: \(key), tvalue: \(value)")
if let value = value as? [String: Any] {
let anyData = try JSONSerialization.data(withJSONObject: value, options: .prettyPrinted)
let anyJson = try JSONSerialization.jsonObject(with: anyData) as! [String: Any]
for (key2, value2) in anyJson {
print("key: \(key), object-key: \(key2), object-value: \(value2)")
}
}
}
}
if let arrays = json["someNestedArray"] as? [String: Any] {
for (key, value) in arrays {
print("key: \(key), value: \(value)")
}
}
Thanks in advance!
It's hard to tell if this solves your exact use case since I'm not sure what the purpose of the data is, and how you're anticipating using it, but both of the below solutions correctly decode your JSON into a more usable Swift object.
The simplest way is to model it exactly as the data structure you presented. For example it looks like someNumbers is an optional dictionary keyed by String, with Int values: [String: Int]?.
struct TopLevel: Decodable {
var someNumbers: [String: Int]?
var someNestedAny: [String: [String: Int]]?
var someNestedArray: [String: [String]]?
}
For a little more readability when passing objects around you can throw in some type aliases and it becomes
typealias SomeNumbers = [String: Int]
typealias SomeNestedAny = [String: [String: Int]]
typealias SomeNestedArray = [String: [String]]
struct TopLevel: Decodable {
var someNumbers: SomeNumbers?
var someNestedAny: SomeNestedAny?
var someNestedArray: SomeNestedArray?
}
To get useful things out you'll then need to call things like
topLevel.someNumbers?["22"] // 6
topLevel.someNestedAny?["8310"] // ["desktop": 2]
topLevel.someNestedAny?["8310"]?["desktop"] // 2
topLevel.someNestedArray?["52"] // ["browser"]
topLevel.someNestedArray?["52"]?[0] // "browser"
Or depending on your needs it may make more sense to loop through things
topLevel.someNestedAny?
.forEach { item in
print("|- \(item.key)")
item.value.forEach { any in
print("| |- \(any.key)")
print("| | |- \(any.value)")
}
}
// |- 61
// | |- browser
// | | |- 2
// |- 8310
// | |- desktop
// | | |- 2

Is it possible to decode additional parameters using JSONDecoder?

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

Swift 4 JSON decode with configurable keys

I'm new to Swift and I need to parse a JSON with some configurable keys.
Opposite to many examples I've seen here, the keys are known before the decode operation is started, they just depend on some parameters passed to endpoint.
Example:
https://some.provider.com/endpoint/?param=XXX
and
https://some.provider.com/endpoint/?param=YYY
will answer, respectively:
[
{
"fixed_key1": "value1",
"fixed_key2": "value2",
"variable_key_1_XXX": "some value",
"variable_key_2_XXX": "some other value"
},
...
]
and
[
{
"fixed_key1": "value1",
"fixed_key2": "value2",
"variable_key_1_YYY": "some value",
"variable_key_2_YYY": "some other value"
},
...
]
Given that those keys are known before decoding, I was hoping to get away with some clever declaration of a Decodable structure and/or CodingKeys, without the need to write the
init(from decoder: Decoder)
Unfortunately, I was not able to come up with such a declaration.
Of course I don't want to write one Decodable/CodingKeys structure for every possible parameter value :-)
Any suggestion ?
Unless all your JSON keys are compile-time constants, the compiler can't synthesize the decoding methods. But there are a few things you can do to make manual decoding a lot less cumbersome.
First, some helper structs and extensions:
/*
Allow us to initialize a `CodingUserInfoKey` with a `String` so that we can write:
decoder.userInfo = ["param": "XXX"]
Instead of:
decoder.userInfo = [CodingUserInfoKey(rawValue:"param")!: "XXX"]
*/
extension CodingUserInfoKey: ExpressibleByStringLiteral {
public typealias StringLiteralType = String
public init(stringLiteral value: StringLiteralType) {
self.rawValue = value
}
}
/*
This struct is a plain-vanilla implementation of the `CodingKey` protocol. Adding
`ExpressibleByStringLiteral` allows us to initialize a new instance of
`GenericCodingKeys` with a `String` literal, for example:
try container.decode(String.self, forKey: "fixed_key1")
Instead of:
try container.decode(String.self, forKey: GenericCodingKeys(stringValue: "fixed_key1")!)
*/
struct GenericCodingKeys: CodingKey, ExpressibleByStringLiteral {
// MARK: CodingKey
var stringValue: String
var intValue: Int?
init?(stringValue: String) { self.stringValue = stringValue }
init?(intValue: Int) { return nil }
// MARK: ExpressibleByStringLiteral
typealias StringLiteralType = String
init(stringLiteral: StringLiteralType) { self.stringValue = stringLiteral }
}
Then the manual decoding:
struct MyDataModel: Decodable {
var fixedKey1: String
var fixedKey2: String
var variableKey1: String
var variableKey2: String
enum DecodingError: Error {
case missingParamKey
case unrecognizedParamValue(String)
}
init(from decoder: Decoder) throws {
let container = try decoder.container(keyedBy: GenericCodingKeys.self)
// Decode the fixed keys
self.fixedKey1 = try container.decode(String.self, forKey: "fixed_key1")
self.fixedKey2 = try container.decode(String.self, forKey: "fixed_key2")
// Now decode the variable keys
guard let paramValue = decoder.userInfo["param"] as? String else {
throw DecodingError.missingParamKey
}
switch paramValue {
case "XXX":
self.variableKey1 = try container.decode(String.self, forKey: "variable_key_1_XXX")
self.variableKey2 = try container.decode(String.self, forKey: "variable_key_2_XXX")
case "YYY":
self.variableKey1 = try container.decode(String.self, forKey: "variable_key_1_YYY")
self.variableKey2 = try container.decode(String.self, forKey: "variable_key_2_YYY")
default:
throw DecodingError.unrecognizedParamValue(paramValue)
}
}
}
And finally here's how you use it:
let jsonData = """
[
{
"fixed_key1": "value1",
"fixed_key2": "value2",
"variable_key_1_XXX": "some value",
"variable_key_2_XXX": "some other value"
}
]
""".data(using: .utf8)!
// Supplying the `userInfo` dictionary is how you "configure" the JSON-decoding
let decoder = JSONDecoder()
decoder.userInfo = ["param": "XXX"]
let model = try decoder.decode([MyDataModel].self, from: jsonData)
print(model)
Taking a similar approach to #Code Different's answer, you can pass the given parameter information through the decoder's userInfo dictionary, and then pass this onto the key type that you use to decode from the keyed container.
First, we can define a new static member on CodingUserInfoKey to use as the key in the userInfo dictionary:
extension CodingUserInfoKey {
static let endPointParameter = CodingUserInfoKey(
rawValue: "com.yourapp.endPointParameter"
)!
}
(the force unwrap never fails; I regard the fact the initialiser is failable as a bug).
Then we can define a type for your endpoint parameter, again using static members to abstract away the underlying strings:
// You'll probably want to rename this to something more appropriate for your use case
// (same for the .endPointParameter CodingUserInfoKey).
struct EndpointParameter {
static let xxx = EndpointParameter("XXX")
static let yyy = EndpointParameter("YYY")
// ...
var stringValue: String
init(_ stringValue: String) { self.stringValue = stringValue }
}
Then we can define your data model type:
struct MyDataModel {
var fixedKey1: String
var fixedKey2: String
var variableKey1: String
var variableKey2: String
}
And then make it Decodable like so:
extension MyDataModel : Decodable {
private struct CodingKeys : CodingKey {
static let fixedKey1 = CodingKeys("fixed_key1")
static let fixedKey2 = CodingKeys("fixed_key2")
static func variableKey1(_ param: EndpointParameter) -> CodingKeys {
return CodingKeys("variable_key_1_\(param.stringValue)")
}
static func variableKey2(_ param: EndpointParameter) -> CodingKeys {
return CodingKeys("variable_key_2_\(param.stringValue)")
}
// We're decoding an object, so only accept String keys.
var stringValue: String
var intValue: Int? { return nil }
init?(intValue: Int) { return nil }
init(stringValue: String) { self.stringValue = stringValue }
init(_ stringValue: String) { self.stringValue = stringValue }
}
init(from decoder: Decoder) throws {
guard let param = decoder.userInfo[.endPointParameter] as? EndpointParameter else {
// Feel free to make this a more detailed error.
struct EndpointParameterNotSetError : Error {}
throw EndpointParameterNotSetError()
}
let container = try decoder.container(keyedBy: CodingKeys.self)
self.fixedKey1 = try container.decode(String.self, forKey: .fixedKey1)
self.fixedKey2 = try container.decode(String.self, forKey: .fixedKey2)
self.variableKey1 = try container.decode(String.self, forKey: .variableKey1(param))
self.variableKey2 = try container.decode(String.self, forKey: .variableKey2(param))
}
}
You can see we're defining the fixed keys using static properties on CodingKeys, and for the variable keys we're using static methods that take the given parameter as an argument.
Now you can perform a decode like so:
let jsonString = """
[
{
"fixed_key1": "value1",
"fixed_key2": "value2",
"variable_key_1_XXX": "some value",
"variable_key_2_XXX": "some other value"
}
]
"""
let decoder = JSONDecoder()
decoder.userInfo[.endPointParameter] = EndpointParameter.xxx
do {
let model = try decoder.decode([MyDataModel].self, from: Data(jsonString.utf8))
print(model)
} catch {
print(error)
}
// [MyDataModel(fixedKey1: "foo", fixedKey2: "bar",
// variableKey1: "baz", variableKey2: "qux")]

Swift 4 JSON Decodable simplest way to decode type change

With Swift 4's Codable protocol there's a great level of under the hood date and data conversion strategies.
Given the JSON:
{
"name": "Bob",
"age": 25,
"tax_rate": "4.25"
}
I want to coerce it into the following structure
struct ExampleJson: Decodable {
var name: String
var age: Int
var taxRate: Float
enum CodingKeys: String, CodingKey {
case name, age
case taxRate = "tax_rate"
}
}
The Date Decoding Strategy can convert a String based date into a Date.
Is there something that does that with a String based Float
Otherwise I've been stuck with using CodingKey to bring in a String and use a computing get:
enum CodingKeys: String, CodingKey {
case name, age
case sTaxRate = "tax_rate"
}
var sTaxRate: String
var taxRate: Float { return Float(sTaxRate) ?? 0.0 }
This sort of strands me doing more maintenance than it seems should be needed.
Is this the simplest manner or is there something similar to DateDecodingStrategy for other type conversions?
Update: I should note: I've also gone the route of overriding
init(from decoder:Decoder)
But that is in the opposite direction as it forces me to do it all for myself.
Using Swift 5.1, you may choose one of the three following ways in order to solve your problem.
#1. Using Decodable init(from:) initializer
Use this strategy when you need to convert from String to Float for a single struct, enum or class.
import Foundation
struct ExampleJson: Decodable {
var name: String
var age: Int
var taxRate: Float
enum CodingKeys: String, CodingKey {
case name, age, taxRate = "tax_rate"
}
init(from decoder: Decoder) throws {
let container = try decoder.container(keyedBy: CodingKeys.self)
name = try container.decode(String.self, forKey: CodingKeys.name)
age = try container.decode(Int.self, forKey: CodingKeys.age)
let taxRateString = try container.decode(String.self, forKey: CodingKeys.taxRate)
guard let taxRateFloat = Float(taxRateString) else {
let context = DecodingError.Context(codingPath: container.codingPath + [CodingKeys.taxRate], debugDescription: "Could not parse json key to a Float object")
throw DecodingError.dataCorrupted(context)
}
taxRate = taxRateFloat
}
}
Usage:
import Foundation
let jsonString = """
{
"name": "Bob",
"age": 25,
"tax_rate": "4.25"
}
"""
let data = jsonString.data(using: String.Encoding.utf8)!
let decoder = JSONDecoder()
let exampleJson = try! decoder.decode(ExampleJson.self, from: data)
dump(exampleJson)
/*
prints:
▿ __lldb_expr_126.ExampleJson
- name: "Bob"
- age: 25
- taxRate: 4.25
*/
#2. Using an intermediate model
Use this strategy when you have many nested keys in your JSON or when you need to convert many keys (e.g. from String to Float) from your JSON.
import Foundation
fileprivate struct PrivateExampleJson: Decodable {
var name: String
var age: Int
var taxRate: String
enum CodingKeys: String, CodingKey {
case name, age, taxRate = "tax_rate"
}
}
struct ExampleJson: Decodable {
var name: String
var age: Int
var taxRate: Float
init(from decoder: Decoder) throws {
let privateExampleJson = try PrivateExampleJson(from: decoder)
name = privateExampleJson.name
age = privateExampleJson.age
guard let convertedTaxRate = Float(privateExampleJson.taxRate) else {
let context = DecodingError.Context(codingPath: [], debugDescription: "Could not parse json key to a Float object")
throw DecodingError.dataCorrupted(context)
}
taxRate = convertedTaxRate
}
}
Usage:
import Foundation
let jsonString = """
{
"name": "Bob",
"age": 25,
"tax_rate": "4.25"
}
"""
let data = jsonString.data(using: String.Encoding.utf8)!
let decoder = JSONDecoder()
let exampleJson = try! decoder.decode(ExampleJson.self, from: data)
dump(exampleJson)
/*
prints:
▿ __lldb_expr_126.ExampleJson
- name: "Bob"
- age: 25
- taxRate: 4.25
*/
#3. Using a KeyedDecodingContainer extension method
Use this strategy when converting from some JSON keys' types to your model's property types (e.g. String to Float) is a common pattern in your application.
import Foundation
extension KeyedDecodingContainer {
func decode(_ type: Float.Type, forKey key: Key) throws -> Float {
if let stringValue = try? self.decode(String.self, forKey: key) {
guard let floatValue = Float(stringValue) else {
let context = DecodingError.Context(codingPath: codingPath, debugDescription: "Could not parse json key to a Float object")
throw DecodingError.dataCorrupted(context)
}
return floatValue
} else {
let doubleValue = try self.decode(Double.self, forKey: key)
return Float(doubleValue)
}
}
}
struct ExampleJson: Decodable {
var name: String
var age: Int
var taxRate: Float
enum CodingKeys: String, CodingKey {
case name, age, taxRate = "tax_rate"
}
}
Usage:
import Foundation
let jsonString = """
{
"name": "Bob",
"age": 25,
"tax_rate": "4.25"
}
"""
let data = jsonString.data(using: String.Encoding.utf8)!
let decoder = JSONDecoder()
let exampleJson = try! decoder.decode(ExampleJson.self, from: data)
dump(exampleJson)
/*
prints:
▿ __lldb_expr_126.ExampleJson
- name: "Bob"
- age: 25
- taxRate: 4.25
*/
Unfortunately, I don't believe such an option exists in the current JSONDecoder API. There only exists an option in order to convert exceptional floating-point values to and from a string representation.
Another possible solution to decoding manually is to define a Codable wrapper type for any LosslessStringConvertible that can encode to and decode from its String representation:
struct StringCodableMap<Decoded : LosslessStringConvertible> : Codable {
var decoded: Decoded
init(_ decoded: Decoded) {
self.decoded = decoded
}
init(from decoder: Decoder) throws {
let container = try decoder.singleValueContainer()
let decodedString = try container.decode(String.self)
guard let decoded = Decoded(decodedString) else {
throw DecodingError.dataCorruptedError(
in: container, debugDescription: """
The string \(decodedString) is not representable as a \(Decoded.self)
"""
)
}
self.decoded = decoded
}
func encode(to encoder: Encoder) throws {
var container = encoder.singleValueContainer()
try container.encode(decoded.description)
}
}
Then you can just have a property of this type and use the auto-generated Codable conformance:
struct Example : Codable {
var name: String
var age: Int
var taxRate: StringCodableMap<Float>
private enum CodingKeys: String, CodingKey {
case name, age
case taxRate = "tax_rate"
}
}
Although unfortunately, now you have to talk in terms of taxRate.decoded in order to interact with the Float value.
However you could always define a simple forwarding computed property in order to alleviate this:
struct Example : Codable {
var name: String
var age: Int
private var _taxRate: StringCodableMap<Float>
var taxRate: Float {
get { return _taxRate.decoded }
set { _taxRate.decoded = newValue }
}
private enum CodingKeys: String, CodingKey {
case name, age
case _taxRate = "tax_rate"
}
}
Although this still isn't as a slick as it really should be – hopefully a later version of the JSONDecoder API will include more custom decoding options, or else have the ability to express type conversions within the Codable API itself.
However one advantage of creating the wrapper type is that it can also be used in order to make manual decoding and encoding simpler. For example, with manual decoding:
struct Example : Decodable {
var name: String
var age: Int
var taxRate: Float
private enum CodingKeys: String, CodingKey {
case name, age
case taxRate = "tax_rate"
}
init(from decoder: Decoder) throws {
let container = try decoder.container(keyedBy: CodingKeys.self)
self.name = try container.decode(String.self, forKey: .name)
self.age = try container.decode(Int.self, forKey: .age)
self.taxRate = try container.decode(StringCodableMap<Float>.self,
forKey: .taxRate).decoded
}
}
You can always decode manually. So, given:
{
"name": "Bob",
"age": 25,
"tax_rate": "4.25"
}
You can do:
struct Example: Codable {
let name: String
let age: Int
let taxRate: Float
init(from decoder: Decoder) throws {
let values = try decoder.container(keyedBy: CodingKeys.self)
name = try values.decode(String.self, forKey: .name)
age = try values.decode(Int.self, forKey: .age)
guard let rate = try Float(values.decode(String.self, forKey: .taxRate)) else {
throw DecodingError.dataCorrupted(.init(codingPath: [CodingKeys.taxRate], debugDescription: "Expecting string representation of Float"))
}
taxRate = rate
}
enum CodingKeys: String, CodingKey {
case name, age
case taxRate = "tax_rate"
}
}
See Encode and Decode Manually in Encoding and Decoding Custom Types.
But I agree, that it seems like there should be a more elegant string conversion process equivalent to DateDecodingStrategy given how many JSON sources out there incorrectly return numeric values as strings.
I know that this is a really late answer, but I started working on Codable couple of days back only. And I bumped into a similar issue.
In order to convert the string to floating number, you can write an extension to KeyedDecodingContainer and call the method in the extension from init(from decoder: Decoder){}
For the problem mentioned in this issue, see the extension I wrote below;
extension KeyedDecodingContainer {
func decodeIfPresent(_ type: Float.Type, forKey key: K, transformFrom: String.Type) throws -> Float? {
guard let value = try decodeIfPresent(transformFrom, forKey: key) else {
return nil
}
return Float(value)
}
func decode(_ type: Float.Type, forKey key: K, transformFrom: String.Type) throws -> Float {
guard let valueAsString = try? decode(transformFrom, forKey: key),
let value = Float(valueAsString) else {
throw DecodingError.typeMismatch(
type,
DecodingError.Context(
codingPath: codingPath,
debugDescription: "Decoding of \(type) from \(transformFrom) failed"
)
)
}
return value
}
}
You can call this method from init(from decoder: Decoder) method. See an example below;
init(from decoder: Decoder) throws {
let container = try decoder.container(keyedBy: CodingKeys.self)
taxRate = try container.decodeIfPresent(Float.self, forKey: .taxRate, transformFrom: String.self)
}
In fact, you can use this approach to convert any type of data to any other type. You can convert string to Date, string to bool, string to float, float to int etc.
Actually to convert a string to Date object, I will prefer this approach over JSONEncoder().dateEncodingStrategy because if you write it properly, you can include different date formats in the same response.
Hope I helped.
Updated the decode method to return non-optional on suggestion from #Neil.
I used Suran's version, but updated it to return non-optional value for decode(). To me this is the most elegant version. Swift 5.2.
extension KeyedDecodingContainer {
func decodeIfPresent(_ type: Float.Type, forKey key: K, transformFrom: String.Type) throws -> Float? {
guard let value = try decodeIfPresent(transformFrom, forKey: key) else {
return nil
}
return Float(value)
}
func decode(_ type: Float.Type, forKey key: K, transformFrom: String.Type) throws -> Float {
guard let str = try? decode(transformFrom, forKey: key),
let value = Float(str) else {
throw DecodingError.typeMismatch(Int.self, DecodingError.Context(codingPath: codingPath, debugDescription: "Decoding of \(type) from \(transformFrom) failed"))
}
return value
}
}
You can use lazy var to convert the property to another type:
struct ExampleJson: Decodable {
var name: String
var age: Int
lazy var taxRate: Float = {
Float(self.tax_rate)!
}()
private var tax_rate: String
}
One disadvantage of this approach is that you cannot define a let constant if you want to access taxRate, since the first time you access it, you are mutating the struct.
// Cannot use `let` here
var example = try! JSONDecoder().decode(ExampleJson.self, from: data)
The options above only deal with the situation that the given field is always String. Many times I've met APIs where the output was once a string, other times number. So this is my suggestion to solve this. It is up to you to alter this to throw exception or set the decoded value to nil.
var json = """
{
"title": "Apple",
"id": "20"
}
""";
var jsonWithInt = """
{
"title": "Apple",
"id": 20
}
""";
struct DecodableNumberFromStringToo<T: LosslessStringConvertible & Decodable & Numeric>: Decodable {
var value: T
init(from decoder: Decoder) {
print("Decoding")
if let container = try? decoder.singleValueContainer() {
if let val = try? container.decode(T.self) {
value = val
return
}
if let str = try? container.decode(String.self) {
value = T.init(str) ?? T.zero
return
}
}
value = T.zero
}
}
struct MyData: Decodable {
let title: String
let _id: DecodableNumberFromStringToo<Int>
enum CodingKeys: String, CodingKey {
case title, _id = "id"
}
var id: Int {
return _id.value
}
}
do {
let parsedJson = try JSONDecoder().decode(MyData.self, from: json.data(using: .utf8)!)
print(parsedJson.id)
} catch {
print(error as? DecodingError)
}
do {
let parsedJson = try JSONDecoder().decode(MyData.self, from: jsonWithInt.data(using: .utf8)!)
print(parsedJson.id)
} catch {
print(error as? DecodingError)
}
How to used JSONDecodable in Swift 4:
Get the JSON Response and Create Struct
Conform Decodable class in Struct
Other steps in this GitHub project, a simple example