Unable to use a Protocol as a general type

Hey Folks,

I'm about two days in with Swift coming from a Java background.

So here's the issue I'm currently tackling.

I have a general class that I will be using to serialize to JSON and dispatch to a backend service.
There are entities that will be encoded that I will not know at runtime. All I care about is that they
conform to the Encodable protocol.

So I've done something like:

public class Filter : Encodable {
    
    fileprivate var name:        FilterName
    fileprivate var filter:      Filter?
    fileprivate var filters:     [Filter]?
    fileprivate var filterLeft:  Filter?
    fileprivate var filterRight: Filter?
    fileprivate var property:    String?
    fileprivate var value:       Encodable?
    fileprivate var values:      [Encodable]?
    fileprivate var key:         Encodable?
    fileprivate var from:        Encodable?
    fileprivate var to:          Encodable?
    fileprivate var pattern:     String?
    fileprivate var ignoreCase:  Bool?

The class then conforms to the Encodable protocol with the following:

public func encode(to encoder: Encoder) throws {
        var container = encoder.container(keyedBy: CodingKeys.self)
        try container.encode(BASE_PACKAGE + name.rawValue, forKey: .clazz)
        try container.encodeIfPresent(filter, forKey: .filter)
        try container.encodeIfPresent(filterLeft, forKey: .filterLeft)
        try container.encodeIfPresent(filterRight, forKey: .filterRight)
        try container.encodeIfPresent(property, forKey: .property)
        try container.encodeIfPresent(from, forKey: .from)
        try container.encodeIfPresent(to, forKey: .to)
        try container.encodeIfPresent(value, forKey: .value)
        try container.encodeIfPresent(values, forKey: .values)
        try container.encodeIfPresent(pattern, forKey: .pattern)
        try container.encodeIfPresent(ignoreCase, forKey: .ignoreCase)
    }

Everything compiles fine, but when I run the test, any attempt to process a class member of type Encodable
fails with:

EXC_BAD_INSTRUCTION (code=EXC_I386_INVOP, subcode=0x0)

It appears that using Encodable at runtime like this is invalid, though I haven't clued in as to why.

Anyone have some insights to share? Is there a better approach?

I'd assume that encodeifPresent(_:forKey:) does a bit cast to whatever type you specify. A type derived from Encodable has a different layout than Encodable itself, so if the Encoder reads one, it can't cast it to an Encodable causing it to throw an exception.

Yes, seems so. I've worked around it, but what I found interesting was a more useful error message was provided when running the tests from the command line vs xcode.

Good learning experience.

That's because this checking is done at runtime. It's a necessary evil if you're bridging a weakly typed data structure with a strongly typed language.

The fact that Encodable? conforms to Encodable is due to the lack of conditional conformance and will produce a compile-time error in Swift 4.1. (Generic Encodable containers don't actually conform to Encodable, but until Swift 4.1, there's no way to say that an Optional conforms to Encodable only if the thing it wraps conforms to Encodable, so they just say it always conforms and added a fatalError if it doesn't actually)

As for how to encode your Encodable? types, you can encode an Encodable? using try from?.encode(to: container.superEncoder(forKey: .from)), which I don't think is quite how the API was supposed to be used, but does work. Just know that there's no way to decode something that was encoded this way, since it doesn't include the information about what type the encoded object was so Swift will have no clue what type to instantiate on decode. For the [Encodable]?, you can do

if let values = values {
	var arraycontainer = container.nestedUnkeyedContainer(forKey: .values)
	for value in values {
		try value.encode(to: arraycontainer.superEncoder())
	}
}
1 Like