3

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"

1
  • You could define two more structs i.e ObjectQuotes and ElementsQuotes and be done with it. It would be future proof as well in case if that endpoint gains extra fields that are missing in the original response. Commented Oct 11, 2022 at 9:23

3 Answers 3

3

An easy way to encode different keys is to specify 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

Sign up to request clarification or add additional context in comments.

Comments

2

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
}

1 Comment

I think the user doesn't have that issue: different keys when decoding. The user wants different encoding and decoding keys.
2

UPDATE Added encoding code

You can try something like this:

extension String: Error {}

struct Object: Codable {
  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)
    self.elements = try container.decode(Elements.self, forKey: .elements)
  }

  func encode(to encoder: Encoder) throws {
    var container = encoder.container(keyedBy: CodingKeys.self)
    try container.encode(self.id, forKey: .id)
    try container.encode(self.elements, forKey: .elementQuotes)
  }

}

struct Elements: Codable {
  let values: [Value]

  enum CodingKeys: String, CodingKey {
    case values
    case valueQuotes
  }

  init(from decoder: Decoder) throws {
    let container = try decoder.container(keyedBy: CodingKeys.self)
    self.values = try container.decode([Value].self, forKey: .values)
  }

  func encode(to encoder: Encoder) throws {
    var container = encoder.container(keyedBy: CodingKeys.self)
    try container.encode(self.values, forKey: .valueQuotes)
  }
}

struct Value: Codable {
  let id: String
}

let json = """
{
  "id": "123",
  "elements": {
    "values": [
      {
        "id": "1"
      },
      {
        "id": "2"
      }
    ]
  }
}
""".data(using: .utf8)!

let decoder = JSONDecoder()
let object = try decoder.decode(Object.self, from: json)

let encoder = JSONEncoder()
let string = String(data: try encoder.encode(object), encoding: .utf8)

Comments

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.