Skip to content

Instantly share code, notes, and snippets.

@GeekAndDad
Last active October 28, 2019 22:35
Show Gist options
  • Save GeekAndDad/c3b3fb030595b18fe153d526a815128e to your computer and use it in GitHub Desktop.
Save GeekAndDad/c3b3fb030595b18fe153d526a815128e to your computer and use it in GitHub Desktop.
How to make an AnyEncodable protocol that allows you to pass any struct conforming to Encodable to a function.
import Foundation
// based on clever example from Nick Lockwood: https://gist.github.com/nicklockwood/833fabacbc4b2d11ae7c7d4752b8fd18
// I only needed Encodable so trimmed it down to just that.
protocol AnyEncodableValue: Encodable {}
extension AnyEncodableValue {
func encode(to container: inout SingleValueEncodingContainer) throws {
try container.encode(self)
}
}
struct AnyEncodable: Encodable {
let value: AnyEncodableValue
init(_ value: AnyEncodableValue) {
self.value = value
}
func encode(to encoder: Encoder) throws {
var container = encoder.singleValueContainer()
try value.encode(to: &container)
}
}
// Test it out:
struct Bar: AnyEncodableValue {
var bar: Int
}
struct Baz: AnyEncodableValue {
var baz: String
}
let coder = JSONEncoder()
coder.outputFormatting = .prettyPrinted
let values: [AnyEncodableValue] = [
Bar(bar: 5),
Baz(baz: "Hello"),
]
let typeErasedValues = values.map(AnyEncodable.init)
let data = try coder.encode(typeErasedValues)
print(String(data: data, encoding: .utf8)!)
// try as function argument:
func test(value: [AnyEncodable]) throws -> Data {
let coder = JSONEncoder()
coder.outputFormatting = .prettyPrinted
let data = try coder.encode(value)
return data
}
let dd = try test(value: typeErasedValues)
print(String(data: dd, encoding: .utf8)!)
// try single value
let typeErasedValue = AnyEncodable(values[0])
func test(value: AnyEncodable) throws -> Data {
let coder = JSONEncoder()
coder.outputFormatting = .prettyPrinted
let data = try coder.encode(value)
return data
}
let d = try test(value: typeErasedValue)
print(String(data: d, encoding: .utf8)!)
// Single value without the pre-type erasing step required (nice!):
func test(value: AnyEncodableValue) throws -> Data {
let typeErasedValue = AnyEncodable(value)
let coder = JSONEncoder()
coder.outputFormatting = .prettyPrinted
let data = try coder.encode(typeErasedValue)
return data
}
let d3 = try test(value: values[0])
print(String(data: d3, encoding: .utf8)!)
@nicklockwood
Copy link

Proposed simplification:

import Foundation

// Private, since it's only used internally by AnyEncodable and we don't want to pollute a public protocol 
private extension Encodable {
    func encode(to container: inout SingleValueEncodingContainer) throws {
        try container.encode(self)
    }
}

struct AnyEncodable: Encodable {
    let value: Encodable

    init(_ value: Encodable) {
        self.value = value
    }

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

// Test it out:
struct Bar: Encodable {
    var bar: Int
}

struct Baz: Encodable {
    var baz: String
}

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

let values: [Encodable] = [
    Bar(bar: 5),
    Baz(baz: "Hello"),
]

let typeErasedValues = values.map(AnyEncodable.init)
let data = try coder.encode(typeErasedValues)

print(String(data: data, encoding: .utf8)!)

// try as function argument:
func test(value: [AnyEncodable]) throws -> Data {
    let coder = JSONEncoder()
    coder.outputFormatting = .prettyPrinted

    let data = try coder.encode(value)
    return data
}

let dd = try test(value: typeErasedValues)
print(String(data: dd, encoding: .utf8)!)

// try single value
let typeErasedValue = AnyEncodable(values[0])
func test(value: AnyEncodable) throws -> Data {
    let coder = JSONEncoder()
    coder.outputFormatting = .prettyPrinted

    let data = try coder.encode(value)
    return data
}

let d = try test(value: typeErasedValue)
print(String(data: d, encoding: .utf8)!)

// Single value without the pre-type erasing step required (nice!):
func test(value: Encodable) throws -> Data {
    let typeErasedValue = AnyEncodable(value)
    let coder = JSONEncoder()
    coder.outputFormatting = .prettyPrinted

    let data = try coder.encode(typeErasedValue)
    return data
}

let d3 = try test(value: values[0])
print(String(data: d3, encoding: .utf8)!)

@GeekAndDad
Copy link
Author

Nice! I don't think about extending existing built-in types enough yet. And making that extension private to avoid polluting namespace is a nice tip. (What happens if someone else declares the same function in a public or private extension? Hmm. Now I have to go test that :)

@nicklockwood
Copy link

What happens if someone else declares the same function in a public or private extension?

As long as it's not annotated with @objc it's fine for several different files to define the same private extension function, even within the same module. Such is the joy of Swift's proper access levels and name-spacing (as opposed to C-style privacy-by-obscurity).

@GeekAndDad
Copy link
Author

Added test of normal Encodable types which I didn't do properly late last night:

// Single value without the pre-type erasing step required (nice!):

func testV(value: Encodable) throws -> Data {
	let typeErasedValue = AnyEncodable(value)
	let coder = JSONEncoder()
	coder.outputFormatting = .prettyPrinted
	coder.dateEncodingStrategy = .iso8601

	let data = try coder.encode(typeErasedValue)
	return data
}

print("\ntestV\n")

let s = "Hello!"
let now = Date()

let d3 = try testV(value: s)
print(String(data: d3, encoding: .utf8)!)

let d4 = try testV(value: now)
print(String(data: d4, encoding: .utf8)!)

output:

testV

"Hello!"
"2019-10-28T03:11:11Z"

@GeekAndDad
Copy link
Author

Ah, this made me realize that this doesn't work for my case. I need keyed encoding which requires the generic which causes the problem. doh!

@nicklockwood
Copy link

I need keyed encoding which requires the generic which causes the problem. doh!

I'm not sure what you mean by this. String and Date are encoded with single-value containers by default, but a more complex object will be encoded as a dictionary using keyed encoding. The fact we use a singleValueContainer for the AnyEncodable wrapper won't change that.

@haikusw
Copy link

haikusw commented Oct 28, 2019

Oi! Thanks so much for the reply! Made me look again and realize my test code was flawed (grrr). Sure enough! As you said, it works great:

struct User: Encodable {
	var name: String
	var phone: String
	var userID: String
	var joinedDate: Date
}

let user = User(name: "Joe Bob", phone: "15551212", userID: "201910280001", joinedDate: Date())
let typeErasedValue = AnyEncodable(user)

func test(value: AnyEncodable) throws -> Data {
	let coder = JSONEncoder()
	coder.outputFormatting = .prettyPrinted
	coder.dateEncodingStrategy = .iso8601

	let data = try coder.encode(value)
	return data
}

let d = try test(value: typeErasedValue)
print(String(data: d, encoding: .utf8)!)


// Try single value without the pre-type erasing step required:

func testV(value: Encodable) throws -> Data {
	let typeErasedValue = AnyEncodable(value)
	let coder = JSONEncoder()
	coder.outputFormatting = .prettyPrinted
	coder.dateEncodingStrategy = .iso8601

	let data = try coder.encode(typeErasedValue)
	return data
}

print("\ntestV\n")

let d2 = try testV(value: user)
print(String(data: d2, encoding: .utf8)!)

produces:

{
  "phone" : "15551212",
  "userID" : "201910280001",
  "name" : "Joe Bob",
  "joinedDate" : "2019-10-28T22:33:13Z"
}

testV

{
  "phone" : "15551212",
  "userID" : "201910280001",
  "name" : "Joe Bob",
  "joinedDate" : "2019-10-28T22:33:13Z"
}

Bingo! 💥

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment