Parsing JSON with wildcard keys - json

I'm parsing a poorly designed JSON structure in which I can expect to find values being reused as keys pointing to further data. Something like this
{"modificationDate" : "..."
"type" : "...",
"version" : 2,
"manufacturer": "<WILDCARD-ID>"
"<WILDCARD-ID>": { /* known structure */ } }
WILDCARD-ID can be just about anything at runtime, so I can't map it to a field in a struct somewhere at compile time. But once I dereference that field, its value has known structure, at which point I can follow the usual procedure for mapping JSON to structs.
I've found myself going down this path
let json = try JSONSerialization.jsonObject(with: data, options: .allowFragments) as? [String: Any]
let manDict = json["manufacturer"]
let data = NSKeyedArchiver.archivedData(withRootObject: manDict)
// now you have data!
but this seems very circuitous, which makes me think that maybe there's a cleaner way of accomplishing this?

You can use custom keys with Decodable, like so:
let json = """
{
"modificationDate" : "...",
"type" : "...",
"version" : 2,
"manufacturer": "<WILDCARD-ID>",
"<WILDCARD-ID>": {
"foo": 1
}
}
""".data(using: .utf8)!
struct InnerStruct: Decodable { // just as an example
let foo: Int
}
struct Example: Decodable {
let modificationDate: String
let type: String
let version: Int
let manufacturer: String
let innerData: [String: InnerStruct]
enum CodingKeys: String, CodingKey {
case modificationDate, type, version, manufacturer
}
struct CustomKey: CodingKey {
var stringValue: String
var intValue: Int?
init?(stringValue: String) {
self.stringValue = stringValue
}
init?(intValue: Int) {
self.stringValue = "\(intValue)";
self.intValue = intValue
}
}
init(from decoder: Decoder) throws {
// extract all known properties
let container = try decoder.container(keyedBy: CodingKeys.self)
self.modificationDate = try container.decode(String.self, forKey: .modificationDate)
self.type = try container.decode(String.self, forKey: .type)
self.version = try container.decode(Int.self, forKey: .version)
self.manufacturer = try container.decode(String.self, forKey: .manufacturer)
// get the inner structs with the unknown key(s)
var inner = [String: InnerStruct]()
let customContainer = try decoder.container(keyedBy: CustomKey.self)
for key in customContainer.allKeys {
if let innerValue = try? customContainer.decode(InnerStruct.self, forKey: key) {
inner[key.stringValue] = innerValue
}
}
self.innerData = inner
}
}
do {
let example = try JSONDecoder().decode(Example.self, from: json)
print(example)
}

You can capture the idea of "a specific, but currently unknown key" in a struct:
struct StringKey: CodingKey {
static let modificationDate = StringKey("modificationDate")
static let type = StringKey("type")
static let version = StringKey("version")
static let manufacturer = StringKey("manufacturer")
var stringValue: String
var intValue: Int?
init?(stringValue: String) { self.init(stringValue) }
init?(intValue: Int) { return nil }
init(_ stringValue: String) { self.stringValue = stringValue }
}
With that, decoding is straightforward, and only decodes the structure that matches the key:
init(from decoder: Decoder) throws {
let container = try decoder.container(keyedBy: StringKey.self)
modificationDate = try container.decode(String.self, forKey: .modificationDate)
type = try container.decode(String.self, forKey: .type)
version = try container.decode(Int.self, forKey: .version)
manufacturer = try container.decode(String.self, forKey: .manufacturer)
// Decode the specific key that was identified by `manufacturer`,
// and fail if it's missing
manufacturerData = try container.decode(ManufacturerData.self,
forKey: StringKey(manufacturer))
}

Related

How to resolve the typeMismatch error in SwiftUI?

This is my model. When I fetch the detail the an error occured.
// MARK: - PublisherModel
public struct PublisherModel: Decodable {
public let userslist: [UserslistModel]
}
// MARK: - UserslistModel
public struct UserslistModel: Decodable, Hashable {
public var id: String
public var full_name: String
public var totalBookViews: String
public var totalBooks: String?
public var full_address: String?
private enum CodingKeys: String, CodingKey {
case id,full_name,totalBookViews,totalBooks,full_address
}
public init(from decoder: Decoder) throws {
let container = try decoder.container(keyedBy: CodingKeys.self)
full_name = try container.decode(String.self, forKey: .full_name)
full_address = try container.decode(String.self, forKey: .full_address)
totalBooks = try container.decode(String.self, forKey: .totalBooks)
totalBookViews = try container.decode(String.self, forKey: .totalBookViews)
do {
id = try String(container.decode(Int.self, forKey: .id))
} catch DecodingError.typeMismatch {
id = try container.decode(String.self, forKey: .id)
}
}
}
This is My ViewModel
class PublisherModelVM: ObservableObject {
#Published var datas = [UserslistModel]()
let url = "APIURL***?************/PublisherList" // For Security Reason I deleted The Api URl
init() {
getData(url: url)
}
func getData(url: String) {
guard let url = URL(string: url) else { return }
URLSession.shared.dataTask(with: url) { (data, _, _) in
if let data = data {
do {
let results = try JSONDecoder().decode(PublisherModel.self, from: data).userslist
DispatchQueue.main.async {
self.datas = results.reversed()
}
}
catch {
print(error)
}
}
}.resume()
}
}
This is My View here I fetch the details
struct PublisherDataView: View{
#StateObject var list = PublisherModelVM()
#State var logourl: String?
var body: some View{
ScrollView(.horizontal,showsIndicators: false){
HStack{
ForEach( list.datas, id: \.id){ item in
VStack(spacing: 12){
Text(item.full_name )
Text(item.full_address ?? "-")
Text(item.totalBookViews)
Text(item.totalBooks!)
}
.frame(width:275, height: 220)
.background(Color.brown.opacity(0.5)).cornerRadius(12)
}
}
}
}
}
And when run this code the a error show that is:
Where am I wrong? Is it whenever I changed full_address with String?
Then also this issue appeared: none typeMismatch(Swift.String, Swift.DecodingError.Context(codingPath: [CodingKeys(stringValue: "userslist", intValue: nil), _JSONKey(stringValue: "Index 0", intValue: 0), CodingKeys(stringValue: "totalBooks", intValue: nil)], debugDescription: "Expected to decode String but found a number instead.", underlyingError: nil))
And please help me for resolve this problem with my Code.
First of all please delete all init methods in your structs. The compiler creates them on your behalf, and Codable doesn't use them anyway.
public init(userslist: [UserslistModel]) {
self.userslist = userslist
}
and
public init(id: Int, full_name: String, totalBookViews: String, totalBooks: Int, full_address: String) {
self.id = id
self.full_name = full_name
self.totalBookViews = totalBookViews
self.totalBooks = totalBooks
self.full_address = full_address
}
Second of all add the convertFromSnakeCase strategy to map the snake_case keys to camelCase struct members.
totalBookViews is sometimes Int and sometimes String. It's still more complicating: The string value cannot be directly converted to Int because it contains letters and whitespace characters.
You have to implement init(from decoder and decode totalBookViews conditionally. If it's a string, filter the numeric characters and convert the string to Int.
Another issue – already mentioned by Nirav D – is that fullAddress can be nil. The init method must consider that, too.
// MARK: - PublisherModel
public struct PublisherModel: Decodable {
public let userslist: [UserslistModel]
}
// MARK: - UserslistModel
public struct UserslistModel: Decodable {
public let id: Int
public let fullName: String
public let totalBookViews: Int
public let totalBooks: Int
public let fullAddress: String
private enum CodingKeys: String, CodingKey {
case id, fullName, totalBookViews, totalBooks, fullAddress
}
public init(from decoder: Decoder) throws {
let container = try decoder.container(keyedBy: CodingKeys.self)
self.id = try container.decode(Int.self, forKey: .id)
self.fullName = try container.decode(String.self, forKey: .fullName)
do {
self.totalBookViews = try container.decode(Int.self, forKey: .totalBookViews)
} catch {
let bookViews = try container.decode(String.self, forKey: .totalBookViews).filter(\.isNumber)
guard let bookViewsAmount = Int(bookViews) else {
throw DecodingError.dataCorruptedError(forKey: .totalBookViews, in: container, debugDescription: "Wrong String Format")
}
self.totalBookViews = bookViewsAmount
}
self.totalBooks = try container.decode(Int.self, forKey: .totalBooks)
self.fullAddress = try container.decodeIfPresent(String.self, forKey: .fullAddress) ?? ""
}
}
And create the decoder
let decoder = JSONDecoder()
decoder.keyDecodingStrategy = .convertFromSnakeCase
let results = try decoder.decode(PublisherModel.self, from: data).userslist

ignore null object in array when parse with Codable swift

i'm parsing this API with swift Codable
"total": 7,
"searchResult": [
null,
{
"name": "joe"
"family": "adam"
},
null,
{
"name": "martin"
"family": "lavrix"
},
{
"name": "sarah"
"family": "mia"
},
null,
{
"name": "ali"
"family": "abraham"
}
]
with this PaginationModel:
class PaginationModel<T: Codable>: Codable {
var total: Int?
var data: T?
enum CodingKeys: String, CodingKey {
case total
case data = "searchResult"
}
required init(from decoder: Decoder) throws {
let container = try decoder.container(keyedBy: CodingKeys.self)
self.total = try container.decodeIfPresent(Int.self, forKey: .total)
self.data = try container.decodeIfPresent(T.self, forKey: .data)
}
}
and User Model:
struct User: Codable {
var name: String?
var family: String?
}
i call jsonDecoder like this to parse API json:
let responseObject = try JSONDecoder().decode(PaginationModel<[User?]>.self, from: json)
now my problem is null in searchResult Array. it parsed correctly and when i access to data in paginationModel i found null in array.
how can i ignore all null when parsing API, and result will be an array without any null
In the first place, I would advise to always consider PaginationModel to be composed from arrays. You don't have to pass [User] as the generic type, you can just pass User. Then the parser can use the knowledge that it parses arrays and handle null automatically:
class PaginationModel<T: Codable>: Codable {
var total: Int?
var data: [T]?
enum CodingKeys: String, CodingKey {
case total
case data = "searchResult"
}
required init(from decoder: Decoder) throws {
let container = try decoder.container(keyedBy: CodingKeys.self)
self.total = try container.decodeIfPresent(Int.self, forKey: .total)
self.data = (try container.decodeIfPresent([T?].self, forKey: .data))?.compactMap { $0 }
}
}
You might want to remove optionals here and use some default values instead:
class PaginationModel<T: Codable>: Codable {
var total: Int = 0
var data: [T] = []
enum CodingKeys: String, CodingKey {
case total
case data = "searchResult"
}
required init(from decoder: Decoder) throws {
let container = try decoder.container(keyedBy: CodingKeys.self)
self.total = (try container.decodeIfPresent(Int.self, forKey: .total)) ?? 0
self.data = ((try container.decodeIfPresent([T?].self, forKey: .data)) ?? []).compactMap { $0 }
}
}
Simple solution, filter data after decoding
let responseObject = try JSONDecoder().decode(PaginationModel<[User?]>.self, from: data)
responseObject.data = responseObject.data?.filter{$0 != nil}
You may add an array type check within decode :
required init(from decoder: Decoder) throws {
let container = try decoder.container(keyedBy: CodingKeys.self)
self.total = try container.decodeIfPresent(Int.self, forKey: .total)
self.data = try container.decodeIfPresent(T.self, forKey: .data)
//add the following:
if let array = self.data as? Array<Any?> {
self.data = ( array.compactMap{$0} as? T)
}
}
Note, you can just define the decodable variable that may be null/nil as [Float?] (or whatever type), with the optional '?' inside the array brackets.

How to Add or Remove an object in result of JSON Codable Swift 4 (with dynamic Keys)

i want import JSON in swift with Codable, modify the object by adding or removing object, and export it in JSON.
Here, my structure
class GenericCodingKeys: CodingKey {
var stringValue: String
var intValue: Int?
required init?(stringValue: String) { self.stringValue = stringValue }
required init?(intValue: Int) { self.intValue = intValue; self.stringValue = "\(intValue)" }
}
class ListSpecie : Codable {
var species: [String : Specie]
required init(from decoder: Decoder) throws
{
let container = try decoder.container(keyedBy: GenericCodingKeys.self)
self.species = [String: Specie]()
for key in container.allKeys{
let value = try container.decodeIfPresent(Specie.self, forKey: GenericCodingKeys(stringValue: key.stringValue)!)
self.species[key.stringValue] = value
}
}
}
class Specie : Codable {
var name : String?
var latinName : [String]?
enum CodingKeys: String, CodingKey {
case name = "l"
case latinName = "ll"
}
required init(from decoder: Decoder) throws
{
let sValues = try decoder.container(keyedBy: CodingKeys.self)
name = try sValues.decodeIfPresent(String.self, forKey: .name)
latinName = try sValues.decodeIfPresent(Array<String>.self, forKey: .latinName)
}
func encode(to encoder: Encoder) throws {
var container = encoder.container(keyedBy: CodingKeys.self)
try container.encodeIfPresent(name, forKey: .name)
try container.encodeIfPresent(latinName, forKey: .latinName)
}
}
Here, the is a code with sample JSON
let myJson = """
{
"especeID1": {
"l": "Ail",
"ll": ["Allium sativum L.","Allium"]
},
"especeID2": {
"l": "Artichaut",
"ll": ["Cynara cardunculus"]
}
}
"""
let jsonDATA = myJson.data(using: .utf8)!
do{
self.jsonResult = try JSONDecoder().decode(ListSpecie.self, from: jsonDATA)
}catch{
print(error.localizedDescription)
}
Here, I want append or remove specie Object on jsonResult
for myspecie in (self.jsonResult?.species)! {
print(myspecie.key + " " + myspecie.value.name!)
}
// Encodage
let encoder = JSONEncoder()
let productJSON = try! encoder.encode(self.jsonResult?.species)
let jsonString = String(data: productJSON, encoding: .utf8)!
Someone could tell me how i can append or remove a specie object in my jsonResult variable .
Thanks a lot for the help you can bring me.
First of all your code is too complicated, most of the code is redundant.
One class (consider a struct) is sufficient
class Specie : Codable {
var name : String?
var latinName : [String]?
enum CodingKeys: String, CodingKey {
case name = "l"
case latinName = "ll"
}
}
If name and latin name is supposed to appear everywhere declare the properties non-optional (remove the question marks).
And decode the JSON
self.jsonResult = try JSONDecoder().decode([String:Specie].self, from: jsonDATA)
jsonResult is now a dictionary ([String:Specie]), you can remove items
self.jsonResult.removeValue(forKey: "especeID2")
or add an item
let newSpecies = Specie()
newSpecies.name = "Potato"
newSpecies.latinName = ["Solanum tuberosum"]
self.jsonResult["especeID3"] = newSpecies
and encode the object
let encoder = JSONEncoder()
let productJSON = try! encoder.encode(self.jsonResult)
let jsonString = String(data: productJSON, encoding: .utf8)!

Decoding two different JSON responses in one model class using Codable

Based on the requirement I got two different kinds of response from api. That is
{
"shopname":"xxx",
"quantity":4,
"id":1,
"price":200.00,
}
another response
{
"storename":"xxx",
"qty":4,
"id":1,
"amount":200.00,
}
Here both json values are decoding in same model class. Kindly help me to resolve this concern.
is it is possible to set value in single variable like qty and quantity both are stored in same variable based on key param availability
Here's an approach that lets you have only one property in your code, instead of two Optionals:
Define a struct that contains all the properties you need, with the names that you'd like to use in your code. Then, define two CodingKey enums that map those properties to the two different JSON formats and implement a custom initializer:
let json1 = """
{
"shopname":"xxx",
"quantity":4,
"id":1,
"price":200.00,
}
""".data(using: .utf8)!
let json2 = """
{
"storename":"xxx",
"qty":4,
"id":1,
"amount":200.00,
}
""".data(using: .utf8)!
struct DecodingError: Error {}
struct Model: Decodable {
let storename: String
let quantity: Int
let id: Int
let price: Double
enum CodingKeys1: String, CodingKey {
case storename = "shopname"
case quantity
case id
case price
}
enum CodingKeys2: String, CodingKey {
case storename
case quantity = "qty"
case id
case price = "amount"
}
init(from decoder: Decoder) throws {
let container1 = try decoder.container(keyedBy: CodingKeys1.self)
let container2 = try decoder.container(keyedBy: CodingKeys2.self)
if let storename = try container1.decodeIfPresent(String.self, forKey: CodingKeys1.storename) {
self.storename = storename
self.quantity = try container1.decode(Int.self, forKey: CodingKeys1.quantity)
self.id = try container1.decode(Int.self, forKey: CodingKeys1.id)
self.price = try container1.decode(Double.self, forKey: CodingKeys1.price)
} else if let storename = try container2.decodeIfPresent(String.self, forKey: CodingKeys2.storename) {
self.storename = storename
self.quantity = try container2.decode(Int.self, forKey: CodingKeys2.quantity)
self.id = try container2.decode(Int.self, forKey: CodingKeys2.id)
self.price = try container2.decode(Double.self, forKey: CodingKeys2.price)
} else {
throw DecodingError()
}
}
}
do {
let j1 = try JSONDecoder().decode(Model.self, from: json1)
print(j1)
let j2 = try JSONDecoder().decode(Model.self, from: json2)
print(j2)
} catch {
print(error)
}
Handling different key names in single model
Below are two sample json(dictionaries) that have some common keys (one, two) and a few different keys (which serve the same purpose of error).
Sample json:
let error_json:[String: Any] = [
"error_code": 404, //different
"error_message": "file not found", //different
"one":1, //common
"two":2 //common
]
let failure_json:[String: Any] = [
"failure_code": 404, //different
"failure_message": "file not found", //different
"one":1, //common
"two":2 //common
]
CommonModel
struct CommonModel : Decodable {
var code: Int?
var message: String?
var one:Int //common
var two:Int? //common
private enum CodingKeys: String, CodingKey{ //common
case one, two
}
private enum Error_CodingKeys : String, CodingKey {
case code = "error_code", message = "error_message"
}
private enum Failure_CodingKeys : String, CodingKey {
case code = "failure_code", message = "failure_message"
}
init(from decoder: Decoder) throws {
let commonValues = try decoder.container(keyedBy: CodingKeys.self)
let errors = try decoder.container(keyedBy: Error_CodingKeys.self)
let failures = try decoder.container(keyedBy: Failure_CodingKeys.self)
///common
self.one = try commonValues.decodeIfPresent(Int.self, forKey: .one)!
self.two = try commonValues.decodeIfPresent(Int.self, forKey: .two)
/// different
if errors.allKeys.count > 0{
self.code = try errors.decodeIfPresent(Int.self, forKey: .code)
self.message = try errors.decodeIfPresent(String.self, forKey: .message)
}
if failures.allKeys.count > 0{
self.code = try failures.decodeIfPresent(Int.self, forKey: .code)
self.message = try failures.decodeIfPresent(String.self, forKey: .message)
}
}
}
Below extension will help you to convert your dictionary to data.
public extension Decodable {
init(from: Any) throws {
let data = try JSONSerialization.data(withJSONObject: from, options: .prettyPrinted)
let decoder = JSONDecoder()
self = try decoder.decode(Self.self, from: data)
}
}
Testing
public func Test_codeble(){
do {
let err_obj = try CommonModel(from: error_json)
print(err_obj)
let failed_obj = try CommonModel(from: failure_json)
print(failed_obj)
}catch let error {
print(error.localizedDescription)
}
}
Use like
struct modelClass : Codable {
let amount : Float?
let id : Int?
let price : Float?
let qty : Int?
let quantity : Int?
let shopname : String?
let storename : String?
enum CodingKeys: String, CodingKey {
case amount = "amount"
case id = "id"
case price = "price"
case qty = "qty"
case quantity = "quantity"
case shopname = "shopname"
case storename = "storename"
}
init(from decoder: Decoder) throws {
let values = try decoder.container(keyedBy: CodingKeys.self)
amount = try values.decodeIfPresent(Float.self, forKey: .amount)
id = try values.decodeIfPresent(Int.self, forKey: .id)
price = try values.decodeIfPresent(Float.self, forKey: .price)
qty = try values.decodeIfPresent(Int.self, forKey: .qty)
quantity = try values.decodeIfPresent(Int.self, forKey: .quantity)
shopname = try values.decodeIfPresent(String.self, forKey: .shopname)
storename = try values.decodeIfPresent(String.self, forKey: .storename)
}
}

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")]