问题
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 ?
回答1:
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)
回答2:
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")]
来源:https://stackoverflow.com/questions/48832709/swift-4-json-decode-with-configurable-keys