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.