How to Extend Float3 or Any Other Built-In Type to Conform to the Codable Protocol

How to extend float3 or any other built-in type to conform to the Codable protocol?

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

How can I write a function which accepts any object which conforms to Codable

Your signature is incorrect. You don't want a Codable. You want a generic type that conforms to Codable. Specifically, you only really need one that conforms to Encodable:

func sendToServer<Message: Encodable>(message: Message) { ... }

A "Codable" or "Encodable" (the protocols) can't itself be encoded. It doesn't have any information about what to encode. But types that conform to Encodable provide that information.

Can you “extend” (i.e. add additional initialization logic to) the auto-generated constructor for a Codable object?

What you are looking for is similar to the delegate pattern where the decoder informs its delegate that it has finished decoding. Sadly, it's not yet added to Swift. The closest I can think off is to use inheritance so Swift can auto generate the decoder for the those 50 let in the base class, and you can initialize your computed properties in a subclass. For example:

class A: Decodable {
let firstName: String
let lastName: String
}

class B: A {
private var _fullName: String! = nil
var fullName: String { return _fullName }

required init(from decoder: Decoder) throws {
try super.init(from: decoder)
_fullName = firstName + " " + lastName
}
}

Define your 50 properties in class A and leave all the computed properties in class B.


Or at your suggestion, you can also use lazy var:

struct Model: Decodable {
let firstName: String
let lastName: String

// private(set) so users cannot change value of the
// pre-computed property
lazy private(set) var fullName = self.firstName + " " + self.lastName
}

// But you can't use a let here, since calling fullName
// for the first time will mutate the struct
var model = try JSONDecoder().decode(Model.self, from: json)

JSON encode non primitive types in Swift

You can use the same technique as shown in this answer for the outer array:

import SceneKit

extension simd_float3x3: Codable {
public init(from decoder: Decoder) throws {
var container = try decoder.unkeyedContainer()
try self.init(container.decode([float3].self))
}
public func encode(to encoder: Encoder) throws {
var container = encoder.unkeyedContainer()
try container.encode([columns.0, columns.1, columns.2])
}
}

Playground testing

let simdFloat = simd_float3x3(float3(0, 1, 2), float3(3,4, 5), float3(6, 7, 8))
do {
let data = try JSONEncoder().encode(simdFloat)
let decodedObject = try JSONDecoder().decode(simd_float3x3.self, from: data)
print(decodedObject) // simd_float3x3([[0.0, 1.0, 2.0], [3.0, 4.0, 5.0], [6.0, 7.0, 8.0]])

} catch {
print(error)
}

Is there any protocol that all Swift SIMD float types conform to?

David is going the right direction, such a protocol doesn't exist (simd is from C and C doesn't have protocols, no surprise), but you can just declare one yourself. To make it so you can use +-*/, you have to add them to the protocol:

import simd

protocol ArithmeticType {
func +(lhs: Self, rhs: Self) -> Self
func -(lhs: Self, rhs: Self) -> Self
func *(lhs: Self, rhs: Self) -> Self
func /(lhs: Self, rhs: Self) -> Self
}

extension float4 : ArithmeticType {}
extension float3 : ArithmeticType {}
extension float2 : ArithmeticType {}

func sum<T: ArithmeticType>(a: T, b: T) -> T {
return a + b
}

You can also extend Double, Float, Int, etc. if you need to

Encode/Decode Array of Types conforming to protocol with JSONEncoder

The reason why your first example doesn't compile (and your second crashes) is because protocols don't conform to themselves – Tag is not a type that conforms to Codable, therefore neither is [Tag]. Therefore Article doesn't get an auto-generated Codable conformance, as not all of its properties conform to Codable.

Encoding and decoding only the properties listed in the protocol

If you just want to encode and decode the properties listed in the protocol, one solution would be to simply use an AnyTag type-eraser that just holds those properties, and can then provide the Codable conformance.

You can then have Article hold an array of this type-erased wrapper, rather than of Tag:

struct AnyTag : Tag, Codable {

let type: String
let value: String

init(_ base: Tag) {
self.type = base.type
self.value = base.value
}
}

struct Article: Codable {
let tags: [AnyTag]
let title: String
}

let tags: [Tag] = [
AuthorTag(value: "Author Tag Value"),
GenreTag(value:"Genre Tag Value")
]

let article = Article(tags: tags.map(AnyTag.init), title: "Article Title")

let jsonEncoder = JSONEncoder()
jsonEncoder.outputFormatting = .prettyPrinted

let jsonData = try jsonEncoder.encode(article)

if let jsonString = String(data: jsonData, encoding: .utf8) {
print(jsonString)
}

Which outputs the following JSON string:

{
"title" : "Article Title",
"tags" : [
{
"type" : "author",
"value" : "Author Tag Value"
},
{
"type" : "genre",
"value" : "Genre Tag Value"
}
]
}

and can be decoded like so:

let decoded = try JSONDecoder().decode(Article.self, from: jsonData)

print(decoded)

// Article(tags: [
// AnyTag(type: "author", value: "Author Tag Value"),
// AnyTag(type: "genre", value: "Genre Tag Value")
// ], title: "Article Title")

Encoding and decoding all properties of the conforming type

If however you need to encode and decoded every property of the given Tag conforming type, you'll likely want to store the type information in the JSON somehow.

I would use an enum in order to do this:

enum TagType : String, Codable {

// be careful not to rename these – the encoding/decoding relies on the string
// values of the cases. If you want the decoding to be reliant on case
// position rather than name, then you can change to enum TagType : Int.
// (the advantage of the String rawValue is that the JSON is more readable)
case author, genre

var metatype: Tag.Type {
switch self {
case .author:
return AuthorTag.self
case .genre:
return GenreTag.self
}
}
}

Which is better than just using plain strings to represent the types, as the compiler can check that we've provided a metatype for each case.

Then you just have to change the Tag protocol such that it requires conforming types to implement a static property that describes their type:

protocol Tag : Codable {
static var type: TagType { get }
var value: String { get }
}

struct AuthorTag : Tag {

static var type = TagType.author
let value: String

var foo: Float
}

struct GenreTag : Tag {

static var type = TagType.genre
let value: String

var baz: String
}

Then we need to adapt the implementation of the type-erased wrapper in order to encode and decode the TagType along with the base Tag:

struct AnyTag : Codable {

var base: Tag

init(_ base: Tag) {
self.base = base
}

private enum CodingKeys : CodingKey {
case type, base
}

init(from decoder: Decoder) throws {
let container = try decoder.container(keyedBy: CodingKeys.self)

let type = try container.decode(TagType.self, forKey: .type)
self.base = try type.metatype.init(from: container.superDecoder(forKey: .base))
}

func encode(to encoder: Encoder) throws {
var container = encoder.container(keyedBy: CodingKeys.self)

try container.encode(type(of: base).type, forKey: .type)
try base.encode(to: container.superEncoder(forKey: .base))
}
}

We're using a super encoder/decoder in order to ensure that the property keys for the given conforming type don't conflict with the key used to encode the type. For example, the encoded JSON will look like this:

{
"type" : "author",
"base" : {
"value" : "Author Tag Value",
"foo" : 56.7
}
}

If however you know there won't be a conflict, and want the properties to be encoded/decoded at the same level as the "type" key, such that the JSON looks like this:

{
"type" : "author",
"value" : "Author Tag Value",
"foo" : 56.7
}

You can pass decoder instead of container.superDecoder(forKey: .base) & encoder instead of container.superEncoder(forKey: .base) in the above code.

As an optional step, we could then customise the Codable implementation of Article such that rather than relying on an auto-generated conformance with the tags property being of type [AnyTag], we can provide our own implementation that boxes up a [Tag] into an [AnyTag] before encoding, and then unbox for decoding:

struct Article {

let tags: [Tag]
let title: String

init(tags: [Tag], title: String) {
self.tags = tags
self.title = title
}
}

extension Article : Codable {

private enum CodingKeys : CodingKey {
case tags, title
}

init(from decoder: Decoder) throws {

let container = try decoder.container(keyedBy: CodingKeys.self)

self.tags = try container.decode([AnyTag].self, forKey: .tags).map { $0.base }
self.title = try container.decode(String.self, forKey: .title)
}

func encode(to encoder: Encoder) throws {
var container = encoder.container(keyedBy: CodingKeys.self)

try container.encode(tags.map(AnyTag.init), forKey: .tags)
try container.encode(title, forKey: .title)
}
}

This then allows us to have the tags property be of type [Tag], rather than [AnyTag].

Now we can encode and decode any Tag conforming type that's listed in our TagType enum:

let tags: [Tag] = [
AuthorTag(value: "Author Tag Value", foo: 56.7),
GenreTag(value:"Genre Tag Value", baz: "hello world")
]

let article = Article(tags: tags, title: "Article Title")

let jsonEncoder = JSONEncoder()
jsonEncoder.outputFormatting = .prettyPrinted

let jsonData = try jsonEncoder.encode(article)

if let jsonString = String(data: jsonData, encoding: .utf8) {
print(jsonString)
}

Which outputs the JSON string:

{
"title" : "Article Title",
"tags" : [
{
"type" : "author",
"base" : {
"value" : "Author Tag Value",
"foo" : 56.7
}
},
{
"type" : "genre",
"base" : {
"value" : "Genre Tag Value",
"baz" : "hello world"
}
}
]
}

and can then be decoded like so:

let decoded = try JSONDecoder().decode(Article.self, from: jsonData)

print(decoded)

// Article(tags: [
// AuthorTag(value: "Author Tag Value", foo: 56.7000008),
// GenreTag(value: "Genre Tag Value", baz: "hello world")
// ],
// title: "Article Title")

Swift 4 JSON Decodable simplest way to decode type change

Unfortunately, I don't believe such an option exists in the current JSONDecoder API. There only exists an option in order to convert exceptional floating-point values to and from a string representation.

Another possible solution to decoding manually is to define a Codable wrapper type for any LosslessStringConvertible that can encode to and decode from its String representation:

struct StringCodableMap<Decoded : LosslessStringConvertible> : Codable {

var decoded: Decoded

init(_ decoded: Decoded) {
self.decoded = decoded
}

init(from decoder: Decoder) throws {

let container = try decoder.singleValueContainer()
let decodedString = try container.decode(String.self)

guard let decoded = Decoded(decodedString) else {
throw DecodingError.dataCorruptedError(
in: container, debugDescription: """
The string \(decodedString) is not representable as a \(Decoded.self)
"""
)
}

self.decoded = decoded
}

func encode(to encoder: Encoder) throws {
var container = encoder.singleValueContainer()
try container.encode(decoded.description)
}
}

Then you can just have a property of this type and use the auto-generated Codable conformance:

struct Example : Codable {

var name: String
var age: Int
var taxRate: StringCodableMap<Float>

private enum CodingKeys: String, CodingKey {
case name, age
case taxRate = "tax_rate"
}
}

Although unfortunately, now you have to talk in terms of taxRate.decoded in order to interact with the Float value.

However you could always define a simple forwarding computed property in order to alleviate this:

struct Example : Codable {

var name: String
var age: Int

private var _taxRate: StringCodableMap<Float>

var taxRate: Float {
get { return _taxRate.decoded }
set { _taxRate.decoded = newValue }
}

private enum CodingKeys: String, CodingKey {
case name, age
case _taxRate = "tax_rate"
}
}

Although this still isn't as a slick as it really should be – hopefully a later version of the JSONDecoder API will include more custom decoding options, or else have the ability to express type conversions within the Codable API itself.

However one advantage of creating the wrapper type is that it can also be used in order to make manual decoding and encoding simpler. For example, with manual decoding:

struct Example : Decodable {

var name: String
var age: Int
var taxRate: Float

private enum CodingKeys: String, CodingKey {
case name, age
case taxRate = "tax_rate"
}

init(from decoder: Decoder) throws {
let container = try decoder.container(keyedBy: CodingKeys.self)

self.name = try container.decode(String.self, forKey: .name)
self.age = try container.decode(Int.self, forKey: .age)
self.taxRate = try container.decode(StringCodableMap<Float>.self,
forKey: .taxRate).decoded
}
}

Override Codable implementation in struct

EDIT after the question was updated

It's true that you can't override SIMD3's codable behaviour in an extension. You can, however, try to wrap your SIMDs in trivial forwarder structs that then allow you to control their encoding:

struct Vector3 { // instead of the typealias
private var simd: SIMD3<Float>
var x: Float { return simd.x }
var y: Float { return simd.y }
var z: Float { return simd.z }
// add forwarders for other SIMD methods/properties you use

init(_ x: Float, _ y: Float, _ z: Float) {
simd = SIMD3(x, y, z)
}
}

extension Vector3: Codable {
enum CodingKeys: String, CodingKey {
case x, y, z
}

init(from decoder: Decoder) throws {
let container = try decoder.container(keyedBy: CodingKeys.self)
let x = try container.decodeIfPresent(Float.self, forKey: .x) ?? 0.0
let y = try container.decodeIfPresent(Float.self, forKey: .y) ?? 0.0
let z = try container.decodeIfPresent(Float.self, forKey: .z) ?? 0.0

self.init(x, y, z)
}
}

(for brevity, I've only included getters and Decodable)


This works as expected:

struct Obj1 : Codable {
var values:[Int]
}

extension Obj1 {
enum CodingKeys: String, CodingKey {
case x, y, z
}

init(from decoder: Decoder) throws {
let container = try decoder.container(keyedBy: CodingKeys.self)
let x = try container.decode(Int.self, forKey: .x)
let y = try container.decode(Int.self, forKey: .y)
let z = try container.decode(Int.self, forKey: .z)

self.values = [x, y, z]
}

func encode(to encoder: Encoder) throws {
var container = encoder.container(keyedBy: CodingKeys.self)
try container.encode(values[0], forKey: .x)
try container.encode(values[1], forKey: .y)
try container.encode(values[2], forKey: .z)
}
}

let obj = Obj1(values: [7, 42, 17])

do {
let data = try JSONEncoder().encode(obj)
print(String(bytes: data, encoding: .utf8)!) // "{"x":7,"y":42,"z":17}"

let o = try JSONDecoder().decode(Obj1.self, from: data)
print(o.values) // [7, 42, 17]
} catch {
print(error)
}

When your implementations are never called, please show the code that encodes/decodes your obj1 instances.



Related Topics



Leave a reply



Submit