Protocol types vs generics (or: encoding an Encodable)

I would like to encode multiple encodables and merge the results. Something like:

func encodeAndMerge(_ values: Codable...) throws -> [String: Any] {
    var result: [String: Any] = [:]
    for value in values {
        let data = try JSONEncoder().encode(value)
        let json = try JSONSerialization.jsonObject(with: data, options: .allowFragments) as? [String: Any]
        result.merge(json, uniquingKeysWith: { $1 })
    }
    return result
}

However, this doesn't work as protocols don't conform to themselves.

Is there a clean solution to this problem?

By clean I mean not using multiple overloads with copy/pasted code, like this:

func encodeAndMerge<T: Codable, U: Codable>(_ first: T, _ second: U) throws -> [String: Any] {
    var result: [String: Any] = [:]
    
    let firstData = try JSONEncoder().encode(first)
    let firstJSON = try JSONSerialization.jsonObject(with: firstData, options: .allowFragments) as! [String: Any]
    result.merge(firstJSON, uniquingKeysWith: { $1 })
    
    let secondData = try JSONEncoder().encode(second)
    let secondJSON = try JSONSerialization.jsonObject(with: secondData, options: .allowFragments) as! [String: Any]
    result.merge(secondJSON, uniquingKeysWith: { $1 })
    
    return result
}

// and so on for 3, 4, ... parameters

If you're trying to encode multiple values of different types conforming to Encodable, the feature you're looking for is called variadic generics, which is not yet implemented.

If you're trying to encode multiple values all of the same type conforming Encodable, the solution is somewhere between what you've written as your two code samples:

func encodeAndMerge<T: Encodable>(_ values: T...) throws -> [String: Any] { /* ... */ }

This package seems like it could help:

It should let you write the method as func encodeAndMerge(_ value: AnyCodable...) throws -> [String: Any] { ... }, with an implementation mostly identical to the example you included.

2 Likes

Thanks Michael.

I didn't know about variadic generics yet, but now I know I want them :slight_smile:

I'm not sure the reason behind this, but wrapping each value in an "AnyEncodable" box will get you what you want.

By "AnyEncodable", I mean something like this:

struct AnyEncodable: Encodable {
	let value: Encodable

	func encode(to encoder: Encoder) throws {
		try value.encode(to: encoder)
	}
}

You can then use it in your encodeAndMerge function:

func encodeAndMerge(_ values: Codable...) throws -> [String: Any] {
	let encoder = JSONEncoder()
	let result: [String: Any] = try values.reduce([String: Any]()) { runningResult, codable in
		let data = try encoder.encode(AnyEncodable(value: codable))
		guard let json = try JSONSerialization.jsonObject(with: data, options: .allowFragments) as? [String: Any] else {
			return runningResult
		}
		return runningResult.merging(json, uniquingKeysWith: { $1 })
	}
	return result
}

Without the box, Swift will give an error:
error: protocol type 'Codable' (aka 'Decodable & Encodable') cannot conform to 'Encodable' because only concrete types can conform to protocols which is what I'm guessing you were getting in your code...

But I don't know exactly why it allows initializing AnyEncodable(value: Encodable) with a protocol type.

Hope this helps.

Note that this has been discussed in other threads (namely, How to encode objects of unknown type?), and an implementation which does

func encode(to encoder: Encoder) throws {
    try value.encode(to: encoder)
}

won't quite give you what you want in all cases because it doesn't give an Encoder a chance to intercept the value before it encodes its contents. This is important for encoding strategies to apply (e.g. JSONEncoder.dateEncodingStrategy), which means that you could have inconsistent application of the strategy across your archive. @hamishknight's approach approach will more consistently give you the results you're looking for.

2 Likes