In trying to serialize an array of float3 objects with the basic JSONEncoder, it\'s revealed that float3 does not conform to the Codable protocol, so this cannot be done.
You can solve the compiler error by instead of trying to directly assign the decoded values to the fields of your type, storing the decoded values in local variables, then calling a designated initializer of float3
.
As Rob mentions in his answer, the cause of the issue has to do with x
, y
and z
being computed properties rather than stored ones, so they cannot be directly written during initialization.
extension float3: Codable {
public init(from decoder: Decoder) throws {
let values = try decoder.container(keyedBy: CodingKeys.self)
let x = try values.decode(Float.self, forKey: .x)
let y = try values.decode(Float.self, forKey: .y)
let z = try values.decode(Float.self, forKey: .z)
self.init(x, y, z)
}
public func encode(to encoder: Encoder) throws {
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)
}
private enum CodingKeys: String, CodingKey {
case x,y,z
}
}
You can test both encoding and decoding in a Playground using below code:
let vector = float3(3, 2.4, 1)
do {
let encodedVector = try JSONEncoder().encode(vector)
let jsonVector = String(data: encodedVector, encoding: .utf8) //"{"x":3,"y":2.4000000953674316,"z":1}"
let decodedVector = try JSONDecoder().decode(float3.self, from: encodedVector) //float3(3.0, 2.4, 1.0)
} catch {
print(error)
}
If you prefer more concise code, the init(from decoder:)
method can be shortened to:
public init(from decoder: Decoder) throws {
let values = try decoder.container(keyedBy: CodingKeys.self)
try self.init(values.decode(Float.self, forKey: .x), values.decode(Float.self, forKey: .y), values.decode(Float.self, forKey: .z))
}
Dávid is correct on how to fix the problem, but it's worth understanding why it's a problem (it's not actually designated vs convenience initializers; that only applies to classes).
If we created our own version of float3
, your code would work fine with an extension:
struct float3 {
var x: Float
var y: Float
var z: Float
}
extension float3: Codable {
init(from decoder: Decoder) throws {
let values = try decoder.container(keyedBy: CodingKeys.self)
x = try values.decode(Float.self, forKey: .x)
y = try values.decode(Float.self, forKey: .y)
z = try values.decode(Float.self, forKey: .z)
}
// ...
}
So that seems strange. Why doesn't our implementation of the simd
type work the same as the simd
type? Because simd
doesn't have an x
stored property (or y
or z
). Those are all computed properties.
The real interface is defined in simd.swift.gyb. If you study that code, you'll see all the SIMD vector types use a generic _vector
storage:
public var _vector: Builtin.${llvm_vectype}
And then there are computed properties defined for each letter (component
is ['x','y','z','w']
):
% for i in xrange(count):
public var ${component[i]} : ${scalar} {
@_transparent
get {
let elt = Builtin.${extractelement}(_vector,
(${i} as Int32)._value)
return ${scalar}(_bits: elt)
}
@_transparent
set {
_vector = Builtin.${insertelement}(_vector,
newValue._value,
(${i} as Int32)._value)
}
}
% end
So if we were building our own float3
(without fancy builtins), it'd be something like this:
struct float3 {
private var vector: [Float]
var x: Float { get { return vector[0] } set { vector[0] = newValue } }
var y: Float { get { return vector[1] } set { vector[1] = newValue } }
var z: Float { get { return vector[2] } set { vector[2] = newValue } }
init(x: Float, y: Float, z: Float) {
vector = [x, y, z]
}
}
And if you write your extension against that, you'll get the same error.
I think that it is worth mentioning that you can simply use an unkeyed container to encode/decode your float3 properties as an array:
extension float3: Codable {
public init(from decoder: Decoder) throws {
var container = try decoder.unkeyedContainer()
try self.init(container.decode([Float].self))
}
public func encode(to encoder: Encoder) throws {
var container = encoder.unkeyedContainer()
try container.encode([x,y,z])
}
}