Search code examples
swiftjsondecoder

Swift JSON decoder with different values


I want to decode a dictionary with different values. So while the key will always be of type String, the value will have the same superclass (like Shape) but might be composed out of different subclasses (like Rectangle, Circle). I want to be able to later check which subclass is attached, but so far I can only use the default decoding into [ AttachedObject: Shape ].

See the example:

enum AttachedObject: String, Codable {
    case chair
    case lamp
    case desk
}

class Shape: Codable {
    var name: String

    init(name: String) {
        self.name = name
    }
}

class Rectangle: Shape {
    var width: Double
    var height: Double

    init(name: String, width: Double, height: Double) {
        self.width = width
        self.height = height
        super.init(name: name)
    }

    enum CodingKeys: String, CodingKey {
        case width
        case height
    }

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

    required public init(from decoder: Decoder) throws {
        let values = try decoder.container(keyedBy: CodingKeys.self)
        self.width = try values.decode(Double.self, forKey: .width)
        self.height = try values.decode(Double.self, forKey: .height)
        try super.init(from: decoder)
    }
}

class Circle: Shape {
    var radius: Double

    init(name: String, radius: Double) {
        self.radius = radius
        super.init(name: name)
    }

    enum CodingKeys: String, CodingKey {
        case radius
    }

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

    required public init(from decoder: Decoder) throws {
        let values = try decoder.container(keyedBy: CodingKeys.self)
        self.radius = try values.decode(Double.self, forKey: .radius)
        try super.init(from: decoder)
    }
}

class MyRoom: Codable {
     public var attachedShapes: [ AttachedObject: Shape ]

     enum CodingKeys: String, CodingKey {
         case attachedShapes
     }

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

     required public init(from decoder: Decoder) throws {
        let values = try decoder.container(keyedBy: CodingKeys.self)
        fatalError("// How to handle the decoding part?")
    }
}

Solution

  • I would go with something like this:

    enum ShapeType: String, RawRepresentable, Codable {
        // Required for RawRepresentable
        static var defaultDecoderValue: ShapeType = .circle
    
        case circle
        case rectangle
    }
    
    struct Shape: Codable {
        let name: String
        let width: Double?
        let height: Double?
        let radius: Double?
        let type: ShapeType
    }
    

    Then you don't need any custom keys. You can always refer to any of the Shape's in arrays, etc. You can look at ShapeType to see whether it's a rect or circle. You can make them var's instead of lets if you need to change them, and you can make is Class Shape instead of Struct Shape if you want a class instead.