Search code examples
swiftjsonencoder

How to encode enum with Arrays of Custom Objects


Thanks for your help in advance! I am relatively new to SwiftUI and have been struggling with encoding an enum with Arrays of custom objects. Here is the code:

struct ChartData: Codable {
    var data: DataType
...
    private enum CodingKeys : String, CodingKey { case id, title, type, info, label_x, label_y, last_update, data }

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

enum DataType: Codable{
    case plot([ChartPlotData])
    case slice([ChartSliceData])
    case number([ChartNumberData])
}

struct ChartPlotData: Codable {
    var chart_id: String
    var order_plot: Int
    var label_plot: String?
    var points_x: [String]
    var points_y: [Double]
    
    private enum CodingKeys : String, CodingKey { case chart_id, order_plot, label_plot, points_x, points_y }
    
    init(from decoder: Decoder) throws {
        let container = try decoder.container(keyedBy: CodingKeys.self)
        self.chart_id = try container.decode(String.self, forKey: .chart_id)
        self.order_plot = try container.decode(Int.self, forKey: .order_plot)
        self.label_plot = try? container.decode(String.self, forKey: .label_plot)
        do{
            self.points_x = try container.decode([String].self, forKey:.points_x)
        }catch{
            let xs = try container.decode([Double].self, forKey:.points_x)
            self.points_x = xs.map { String($0) }
        }
        self.points_y = try container.decode([Double].self, forKey:.points_y)
    }
}

struct ChartSliceData: Codable {
    var chart_id: String
    var order_slice: Int
    var label_slice: String
    var value_slice: Double
}

struct ChartNumberData: Codable {
    var chart_id: String
    var number: Double
    var unit: String?
}

I am attempting to cache JSON in UserDefaults so as to avoid having to make extraneous API calls. Using JSONEncoder, I am left with the following JSON snippet (excerpted from a much longer string):

            "data" : {
              "plot" : {
                "_0" : [
                  {
                    "label_plot" : "China",
                    "points_y" : [
                      0,
                      0,
                      0,
...

However, I am looking to get an encoding like this:

            "data" : [
                  {
                    "label_plot" : "China",
                    "points_y" : [
                      0,
                      0,
                      0,
...

Any help would be greatly appreciated! Thanks so much!


Solution

  • This can be solved by adding an extra item in the CodingKeys enum for encoding and decoding the type used for the DataType enum and then using a switch to encode and decode the right type of array.

    Here is the full ChartData struct although with some properties and code removed for brevity

    struct ChartData: Codable {
        var id: Int
        var title: String
        var data: DataType
       
        enum CodingKeys: String, CodingKey {
            case id, title, data, type
        }
       
        func encode(to encoder: Encoder) throws {
            var container = encoder.container(keyedBy: CodingKeys.self)
            try container.encode(id, forKey: .id)
            try container.encode(title, forKey: .title)
            
            switch data {
            case .number(let values): 
                try container.encode("number", forKey: .type)
                try container.encode(values, forKey: .data)
            case .plot(let values): 
                try container.encode("plot", forKey: .type)
                try container.encode(values, forKey: .data)
            case .slice(let values): 
                try container.encode("slice", forKey: .type)
                try container.encode(values, forKey: .data)
            }
        }
        
        init(from decoder: Decoder) throws {
            var container = try decoder.container(keyedBy: CodingKeys.self)
            id = try container.decode(Int.self, forKey: .id)
            title = try container.decode(String.self, forKey: .title)
            
            let type = try container.decode(String.self, forKey: .type)
            switch type {
            case "number":
                let values = try container.decode([ChartNumberData].self, forKey: .data)
                data = .number(values)
                    // case ...
            default:
                fatalError("Unsupported type for DataType: \(type)")
            }
        }
    }