Home > Software design >  How to properly re-encode a type that is sometimes encoded as an object, and sometimes as an array?
How to properly re-encode a type that is sometimes encoded as an object, and sometimes as an array?

Time:08-06

I am encoding and decoding coordinates that are sometimes encoded as an object {"x":1,"y":2,"z":3} and sometimes as an array [1,2,3].

For decoding, this poses no problem. I'll add the code at the end, but it is rather trivial and uninteresting. However, for re-encoding, I absolutely must be certain the coordinates are encoded back into the exact same way I found them, either object or array.

What is the best way to achieve that ? Should I use a private variable that remembers which kind of source they came from ? Make Coordinates a protocol and have two different structs implement it ? Something else I haven't thought of ?

Please note that I know in advance which encoding is going to be used. The API I'm interfacing with just hasn't updated everything to use objects yet, which is their newest standard as far as I understand. The outcome is not random.


Here is my current decoding logic :

extension Coordinates : Codable {
    private enum CodingKeys : String, CodingKey {
        case x
        case y
        case z
    }
    
    init(from decoder : Decoder) throws {
        if let container = try? decoder.container(keyedBy: CodingKeys.self) {
            
            self.x = try container.decode(Element.self, forKey: .x)
            self.y = try container.decodeIfPresent(Element.self, forKey: .y)
            self.z = try container.decode(Element.self, forKey: .z)
            
        } else if var container = try? decoder.unkeyedContainer() {
            
            self.x = try container.decode(Element.self)
            self.z = try container.decode(Element.self)
            
            if let z = try? container.decode(Element.self) {
                self.y = self.z
                self.z = z
            }
            
        } else {
            preconditionFailure("Cannot decode coordinates")
        }
    }
}

CodePudding user response:

JSONEncoder provides a userInfo dictionary to pass custom data.

As you know in advance which encoding is going to be used take advantage of it.

Declare a custom CodingUserInfoKey and an convenience init method of JSONEncoder

extension CodingUserInfoKey {
    static let encodeAsArray = CodingUserInfoKey(rawValue: "encodeAsArray")!
}

extension JSONEncoder {
    convenience init(encodeAsArray: Bool) {
        self.init()
        self.userInfo[.encodeAsArray] = encodeAsArray
    }
}

Add this encode method to Coordinates

func encode(to encoder: Encoder) throws {
    let userInfo = encoder.userInfo
    if userInfo[.encodeAsArray] as? Bool == true {
        var container = encoder.unkeyedContainer()
        try container.encode([x, y, z])
    } else {
        var container = encoder.container(keyedBy: CodingKeys.self)
        try container.encode(x, forKey: .x)
        try container.encode(y, forKey: .y)
        try container.encode(z, forKey: .z)
    }
}

To use it create the encoder with

let encoder = JSONEncoder(encodeAsArray: true)

CodePudding user response:

You can preserve the structured data structure with a simple enum like:

enum Structured {
    case array([Element])
    case object(Coordination)
    
    private enum CodingKeys : String, CodingKey {
        case x
        case y
        case z
    }
    
    init(from decoder : Decoder) throws {
        if let container = try? decoder.container(keyedBy: CodingKeys.self) {
            let x = try container.decode(Element.self, forKey: .x)
            let y = try container.decodeIfPresent(Element.self, forKey: .y)
            let z = try container.decode(Element.self, forKey: .z)
            self = .object( Coordination(x: x, y: y, z: z) )
        } else if var container = try? decoder.unkeyedContainer() {
            let array = try container.decode([Element].self)
            self = .array( array )
        } else {
            preconditionFailure("Cannot decode coordinates")
        }
    }
}

CodePudding user response:

This is actually an XY problem. Instead of trying to be super smart, I just added an array computed property to Coordinates and will encode that instead of the Coordinates themselves when that is required :

/**
 A spatial position with `XYZ` coordinates, `Y` being optional.
*/
struct Coordinates<Element : Coordinate> : Codable {
    
    /**
     West to East axis coordinate.
     */
    var x : Element
    
    /**
     Down to Up axis coordinate.
     */
    var y : Element?
    
    /**
     North to South axis coordinate.
     */
    var z : Element
    
    var array : [Element] {
        if let y {
            return [x, y, z]
        } else {
            return [x, z]
        }
    }

    /**
     Initialize with all three coordinates.
     */
    init(x : Element, y : Element, z : Element) {
        self.x = x
        self.y = y
        self.z = z
    }
    
    /**
     Initialize with only `x` and `z`.
     */
    init(x : Element, z : Element) {
        self.x = x
        self.y = nil
        self.z = z
    }
}
//Normal encoding
struct Something {
    var coordinates : Coordinates<Int>
    func encode(to encoder: Encoder) throws {
        var container = encoder.unkeyedContainer()
        try container.encode(coordinates)
    }
}

//Array encoding
struct SomethingElse {
    var coordinates : Coordinates<Int>
    func encode(to encoder: Encoder) throws {
        var container = encoder.unkeyedContainer()
        try container.encode(coordinates.array)
    }
}

I don't mind that this forces me to explicitly implement encoding methods, as everything in this file format / API is busted and requires me to custom encode/decode anyways.

  • Related