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)!)
@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