I have to decode an object from this JSON:
{
"id": "..."
"someData": { ... },
"elements": {
"values": [
{
"id": "1",
...
},
{
"id": "2"",
...
}
]
}
}
So I have several structs like this:
struct Object: Codable {
let id: String
let someData: SomeCodableObject
let elements: Elements
}
struct Elements: Codable {
let values: [Value]
}
struct Value: Codable {
let id: String
...
}
After filling each element with some data I've to send an object similar to the decoded one but changing "elements" and "values" by "elementQuotes" and "valueQuotes" respectively (yes, I know the API should avoid this weird behavior, but it's not possible...), that is, a JSON like this:
{
"id": "..."
"someData": { ... },
"elementQuotes": {
"valueQuotes": [
{
"id": "1",
...
},
{
"id": "2"",
...
}
]
}
}
Is there any way to achieve this without using different objects (some for decoding and some other for encoding). That is, is there any way to specify different coding keys string values for encoding/decoding
I repeat: I know this is a really bad practice in the API side but ... I've to manage this "feature"
CodePudding user response:
You can try something like this (I'm only handling decoding here):
extension String: Error {}
struct Object: Decodable {
let id: String
let elements: Elements
enum CodingKeys: String, CodingKey {
case id
case elements
case elementQuotes
}
init(from decoder: Decoder) throws {
let container = try decoder.container(keyedBy: CodingKeys.self)
self.id = try container.decode(String.self, forKey: .id)
let elements = try container.decodeIfPresent(Elements.self, forKey: .elements)
let elementQuotes = try container.decodeIfPresent(Elements.self, forKey: .elementQuotes)
guard let realElements = elements ?? elementQuotes else { throw "Missing elements" }
self.elements = realElements
}
}
struct Elements: Decodable {
let values: [Value]
enum CodingKeys: String, CodingKey {
case values
case valueQuotes
}
init(from decoder: Decoder) throws {
let container = try decoder.container(keyedBy: CodingKeys.self)
let values = try container.decodeIfPresent([Value].self, forKey: .values)
let valueQuotes = try container.decodeIfPresent([Value].self, forKey: .valueQuotes)
self.values = values ?? valueQuotes ?? []
}
}
struct Value: Decodable {
let id: String
}
let test1 = """
{
"id": "123",
"elements": {
"values": [
{
"id": "1"
},
{
"id": "2"
}
]
}
}
""".data(using: .utf8)!
let test2 = """
{
"id": "123",
"elementQuotes": {
"valueQuotes": [
{
"id": "1"
},
{
"id": "2"
}
]
}
}
""".data(using: .utf8)!
let decoder = JSONDecoder()
let json1 = try decoder.decode(Object.self, from: test1)
let json2 = try decoder.decode(Object.self, from: test2)
CodePudding user response:
You can try to write a custom init decoder
struct Root: Decodable {
let id: String
let someData: String
let elements: Elements
enum CodingKeys: String, CodingKey {
case id, someData, elements
case elementQuotes
}
init(from decoder: Decoder) throws {
let container = try decoder.container(keyedBy: CodingKeys.self)
id = try container.decode(String.self, forKey: .id)
someData = try container.decode(String.self, forKey: .someData)
do {
elements = try container.decode(Elements.self, forKey: .elements)
}
catch {
elements = try container.decode(Elements.self, forKey: .elementQuotes)
}
}
}
struct Elements: Decodable {
let values: [Value]
enum CodingKeys: String, CodingKey {
case values
case valuesQuotes
}
init(from decoder: Decoder) throws {
let container = try decoder.container(keyedBy: CodingKeys.self)
do {
values = try container.decode([Value].self, forKey: .values)
}
catch {
values = try container.decode([Value].self, forKey: .valuesQuotes)
}
}
}
struct Value: Codable {
let id: String
}
CodePudding user response:
An easy way to encode different keys add a custom keyEncodingStrategy
.
let encoder = JSONEncoder()
encoder.keyEncodingStrategy = .custom { keys in
switch keys.last!.stringValue {
case "elements": return AnyKey(stringValue: "elementQuotes")!
case "values": return AnyKey(stringValue: "valueQuotes")!
default: return keys.last!
}
}
This requires also an extra struct AnyKey
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
}
}
You can add as many cases as you like in the switch expression