I was dealing with a request that need to modify our iOS model structure, the modification was moving the EncodingOption
parameter to the EditingOptions
class from OutputOptions
struct.
Hierarchy of the config
AppConfig {
profile: ProfileSetting
}
ProfileSetting {
editSetting:EditingOptions
outputSetting: OutputOptions
}
Previous data structure:
class EditingOptions: NSObject, Codable {
//..other parameters
}
struct OutputOptions: Codable {
var barcodeEncoding: EncodingOption = .utf8
//..other parameters
}
Modified data structure:
class EditSetting: NSObject, Codable {
var encodingOption: EncodingOption
//..other parameters
}
struct OutputSetting: Codable {
//..other parameters
}
The modified structure was not compatible with previous config file with the JSON error keyNotFound
:
keyNotFound(CodingKeys(stringValue: "encodingOption", intValue: nil),
Swift.DecodingError.Context(codingPath: [
CodingKeys(stringValue: "profileManage", intValue: nil), CodingKeys(stringValue: "deviceName",
intValue: nil), CodingKeys(stringValue: "profiles", intValue: nil),
_JSONKey(stringValue: "Index 0", intValue: 0),
CodingKeys(stringValue: "outputOptions", intValue: nil)
],
debugDescription: "No value associated with key CodingKeys(stringValue: \"encodingOption\", intValue: nil) (\"encodingOption\").", underlyingError: nil))
The solution from ChatGPT was add a init(from decoder: Decoder)
method for ProfileSetting
class:
required init(from decoder: Decoder) throws {
let container = try decoder.container(keyedBy: CodingKeys.self)
//...other parameters
if let editingOptions = try? container.decode(EditingOptions.self, forKey: .editingOptions) {
self.editingOptions = editingOptions
} else {
self.editingOptions = EditingOptions()
}
}
The barcodeEncoding
was a hidden parameter with a default setting, and not been release to public yet.
The solution did resolved the keyNotFound
error, however, I'm not sured that the solution was the RIGHT way to solve the issue.
If I understood correctly, your issue can be simplified into:
let json = """
{
"a": { "keyA": "Value for KeyA" },
"b": { "keyB": "Value for KeyB",
"otherKey": "My value To Pass to a now "}
}
"""
Where otherKey
is your encodingOption
. It's present on B
, but now you want it to pass it to A
.
So the old code is:
func oldCode(with json: String) {
struct OldParent: Codable {
let a: OldA
let b: OldB
}
class OldA: NSObject, Codable {
let keyA: String
}
struct OldB: Codable {
let keyB: String
let otherKey: String
}
let decoder = JSONDecoder()
do {
let parent = try decoder.decode(OldParent.self, from: Data(json.utf8))
print(parent)
print("--")
} catch {
print("Error: \(error)")
}
}
It works as designed, your iOS model reflects the API model.
Now you want to modify your iOS model, but the API one won't change.
A possible solution is to keep otherKey
in B
, and inside the decoding of the Parent
(which knows both A
and B
), pass that value into A
:
func newCode(with json: String) {
struct NewParent: Codable {
let a: NewA
let b: NewB
init(from decoder: any Decoder) throws {
let container = try decoder.container(keyedBy: CodingKeys.self)
self.a = try container.decode(NewA.self, forKey: .a)
self.b = try container.decode(NewB.self, forKey: .b)
self.a.otherKey = self.b.otherKey //Pass the value
}
}
class NewA: NSObject, Codable {
let keyA: String
var otherKey: String = "defaultValue"
enum CodingKeys: CodingKey {
case keyA //We skip here otherKey to tell the decoder to decode only keyA which is present in the JSON
}
}
struct NewB: Codable {
let keyB: String
let otherKey: String //Only because it's in JSON
}
let decoder = JSONDecoder()
do {
let parent = try decoder.decode(NewParent.self, from: Data(json.utf8))
print(parent)
print("--")
} catch {
print("Error: \(error)")
}
}
Edit: adding another alternative:
If you don't like having let otherKey: String //Only because it's in JSON
in B
and want it only in A
, you can indeed decode it yourself. I didn't do test on efficiency, but I guess it takes a little more time, because it needs to decode again the container for B
(once for otherKey
and the other one for B
, there might be cache, and other optimizations, I haven't fully tested).
func newCode2(with json: String) {
struct NewParent: Codable {
let a: NewA
let b: NewB
init(from decoder: any Decoder) throws {
let container = try decoder.container(keyedBy: CodingKeys.self)
self.a = try container.decode(NewA.self, forKey: .a)
self.b = try container.decode(NewB.self, forKey: .b)
// Here decode again b and look for the value of otherKey
let containerForOtherKeyInB = try container.nestedContainer(keyedBy: CustomKey.self, forKey: .b)
self.a.otherKey = try containerForOtherKeyInB.decode(String.self, forKey: .otherKey)
}
}
class NewA: NSObject, Codable {
let keyA: String
var otherKey: String = "defaultValue"
enum CodingKeys: CodingKey {
case keyA //We skip here otherKey
}
}
enum CustomKey: CodingKey {
case otherKey
}
struct NewB: Codable {
let keyB: String
}
let decoder = JSONDecoder()
do {
let parent = try decoder.decode(NewParent.self, from: Data(json.utf8))
print(parent)
print("--")
} catch {
print("Error: \(error)")
}
}