Decoding swift inheritance data - swift

I have a Process class that has a Step type parameter (Step is "abstract"). I have two Step type classes: StepSign and StepReview, as shown in the code:
class Process: Codable {
let steps: Array<Step>
let actualStep: Step
private enum CodingKeys: String, CodingKey {
case steps, actualStep
}
required init(from decoder: Decoder) throws {
let container = try decoder.container(keyedBy: CodingKeys.self)
steps = try container.decode(Array<Step>.self, forKey: .steps)
actualStep = try container.decode(Step.self, forKey: .actualStep)
}
}
class Step: Codable {
var id : Int?
}
class StepSign : Step {
var name : String?
private enum CodingKeys: String, CodingKey {
case name
}
required init(from decoder: Decoder) throws {
let container = try decoder.container(keyedBy: CodingKeys.self)
name = try container.decode(String.self, forKey: .name)
try super.init(from: decoder)
}
}
class StepReview: Step {
var isReview: Bool
private enum CodingKeys: String, CodingKey {
case isReview
}
required init(from decoder: Decoder) throws {
let container = try decoder.container(keyedBy: CodingKeys.self)
isReview = try container.decode(Bool.self, forKey: .isReview)
try super.init(from: decoder)
}
When I try to decode I get the Step, but I would like it to return either StepSign or StepReview. But I don't know how to indicate what type it is since at the time of decoding I don't know.
The problem I have is in the following lines:
steps = try container.decode(Array<Step>.self, forKey: .steps)
actualStep = try container.decode(Step.self, forKey: .actualStep)
Any ideas?

Related

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!
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)")
}
}
}

Using Decodable with injected properties

Is there any way to use Decodable with injected property?
final class Score: Decodable {
let value: Int?
let uniqueId: String
convenience init(from decoder: Decoder/*, uniqueId: String*/) throws {
let container = try decoder.container(keyedBy: CodingKeys.self)
try container.decodeIfPresent(Int.self, forKey: .value).flatMap { value = $0 }
// self.uniqueId = uniqueId
[... other properties parsing ...]
}
}
Example call:
final class Exam {
let identifier: Int
let scores: [Score]
convenience init(from decoder: Decoder) throws {
let container = try decoder.container(keyedBy: CodingKeys.self)
identifier = try container.decode(Int.self, forKey: .identifier)
scores = try container.decode([Score].self, forKey: .scores)
// I need to pass Exam's identifier to `score` on init, because it will generate Score's `uniqueId `
[... other properties parsing ...]
}
}
That would end with error with missing uniqueId, which I need to have after init but it's not in the JSON. Since it's identifier, making it optional and setting outside is not a proper way to handle it.
I'd love to inject it the way it's commented above, but how to do it?
There is no way to extend the initialiser because it's being called indirectly and there is no API provided to extend it. Thus, there are several ways to bypass it:
BEST: Inject the value into Decoder's userInfo if possible.
Create separate class for Response and separate for model. Example below.
Use plain JSONSerialization instead of Decodable.
As #JoakimDanielson suggested, create random identifier inside default initialiser. The issue is that it's not reproducable, so in case you're saving it to the DB, you'll always override the data, since the ID with each parsing would be different.
Example for approach 2:
final class ScoreResponse: Decodable {
let value: Int?
convenience init(from decoder: Decoder) throws {
let container = try decoder.container(keyedBy: CodingKeys.self)
try container.decodeIfPresent(Int.self, forKey: .value).flatMap { value = $0 }
[... other properties parsing ...]
}
}
final class Score {
let value: Int?
let uniqueId: String
convenience init(from response: ScoreResponse, uniqueId: String) {
self.value = response.value // etc with other properties
self.uniqueId = uniqueId
}
}
final class Exam: Decodable {
let identifier: String
let scores: [Score] = []
convenience init(from decoder: Decoder) throws {
let container = try decoder.container(keyedBy: CodingKeys.self)
identifier = try container.decode(String.self, forKey: .identifier)
try container.decodeIfPresent([ScoreResponse].self, forKey: .scores).forEach {
scores.append({ Score(from: $0, uniqueId: identifier) })
}
}

Error while I'm trying to decode using CodingKeys

This my struct
import Foundation
struct Settings: Hashable, Decodable{
var Id = UUID()
var userNotificationId : Int
}
Coding Keys
private enum CodingKeys: String, CodingKey{
**case userNotificationId = "usuarioNotificacionMovilId"** (this is the line that gets me errors)
}
init
init(userNotificationId: Int){
self.userNotificationId = userNotificationId
}
Decoder
init(from decoder: Decoder) throws{
let container = try decoder.container(keyedBy: CodingKeys.self)
userNotificationId = try container.decodeIfPresent(Int.self, forKey: .userNotificationId) ?? 0
}
Encoder
init(from encoder: Encoder) throws{
var container = encoder.container(keyedBy: CodingKeys.self)
try container.encode(userNotificationId, forKey: .userNotificationId)
}
I get the following error inside the coding method
'self' used before all stored properties are initialized
What is init(from encoder: Encoder) supposed to be? You're not conforming to Encodable, and if you were, you would need to implement func encode(to encoder: Encoder) throws, not another initializer.
That said, your explicit implementation of init(from decoder: Decoder) throws does nothing different from what the compiler would synthesize for you, so it's better to remove it entirely as well.
struct Settings: Hashable, Decodable {
let id = UUID()
let userNotificationId: Int
private enum CodingKeys: String, CodingKey{
case userNotificationId = "usuarioNotificacionMovilId"
}
init(userNotificationId: Int) {
self.userNotificationId = userNotificationId
}
}
is probably all you need.

Swift GEOJson Codable object

I am trying to access coordinates within a GEOJson I receive. I created Codable Structs.
My GEOJson codable Geometry struct is like this:
struct ServiceArea : Codable {
let area : [Area]?
let vehicleType : String?
enum CodingKeys: String, CodingKey {
case area = "area"
case vehicleType = "vehicle_type"
}
init(from decoder: Decoder) throws {
let values = try decoder.container(keyedBy: CodingKeys.self)
area = try values.decodeIfPresent([Area].self, forKey: .area)
vehicleType = try values.decodeIfPresent(String.self, forKey: .vehicleType)
}
}
struct Area : Codable {
let geometry : Geometry?
let properties : Property?
let type : String?
enum CodingKeys: String, CodingKey {
case geometry
case properties
case type = "type"
}
init(from decoder: Decoder) throws {
let values = try decoder.container(keyedBy: CodingKeys.self)
geometry = try Geometry(from: decoder)
properties = try Property(from: decoder)
type = try values.decodeIfPresent(String.self, forKey: .type)
}
}
struct Geometry : Codable {
let coordinates : [[[Float]]]?
let type : String?
enum CodingKeys: String, CodingKey {
case coordinates = "coordinates"
case type = "type"
}
init(from decoder: Decoder) throws {
let values = try decoder.container(keyedBy: CodingKeys.self)
coordinates = try values.decodeIfPresent([[[Float]]].self, forKey: .coordinates)
type = try values.decodeIfPresent(String.self, forKey: .type)
}
}
But when I try to access .coordinates, it is always nil.

Decoding optional List of objects in RealmSwift class

I am trying initialize Codable List of objects in Realm class, the problem is that the app crashes when server return empty list
this is the code for initializing List
class TicketDetails: Object, Decodable {
var working: [WorkingHour]?
var workingHours = List<WorkingHour>()
public convenience required init(from decoder: Decoder) throws {
self.init()
let container = try decoder.container(keyedBy: CodingKeys.self)
if let workingArray = try container.decodeIfPresent(Array<WorkingHour>.self, forKey: .working) {
working = workingArray
workingHours.append(objectsIn: workingArray)
} else {
working = nil
workingHours = List.init()
}
}
}
A few oddities here.
TicketDetails is declared as a Realm object, but includes an array, which is just a duplicate of a Realm List property. Why? Remove the array. List needs to be a let, and remove the entire else clause as that won't work.
class TicketDetails: Object, Decodable
{
let workingHours = List<WorkingHour>()
public convenience required init(from decoder: Decoder) throws {
self.init()
let container = try decoder.container(keyedBy: CodingKeys.self)
if let workingArray = try container.decodeIfPresent(Array<WorkingHour>.self, forKey: .working)
{
workingHours.append(objectsIn: workingArray)
}
}
}
It's more convenient and working solutions with the latest version of RealmCocoa 4.4.1. Don't forget define CodingKeys enum if network responses different than your models.
class TicketDetails: Object, Decodable {
var working = List<[WorkingHour]>()
var workingHours = List<WorkingHour>()
public convenience required init(from decoder: Decoder) throws {
self.init()
let container = try decoder.container(keyedBy: CodingKeys.self)
self.working = try container.decodeIfPresent(List<WorkingHour>.self, forKey: .working) ?? List<WorkingHour>.self
self.workingHours = try container.decodeIfPresent(List<WorkingHour>.self, forKey: .working) ?? List<WorkingHour>.self
}
}