-
Notifications
You must be signed in to change notification settings - Fork 157
Description
Hi! I encountered an error while using the Yams.dump(object:)
function because the represent(:)
tries to cast an Any
value to NodeRepresentable
. And it seems to be a Swift problem.
Problem exposition
The error comes from the deserialisation of a Data
object to Any
. Here is the code that demonstrates that (and the attached playground).
import Foundation
protocol StubProtocol {}
extension Array: StubProtocol {}
let ducks = ["Riri", "Fifi", "Loulou"]
let data = try JSONEncoder().encode(ducks)
let deserializedDucks = try JSONSerialization.jsonObject(with: data)
let castedDeserializedDucks = deserializedDucks as! [String]
print("StubProtocol conformance of 'ducks': \(ducks is StubProtocol)") // true
print("StubProtocol conformance of 'deserializedDucks': \(deserializedDucks is StubProtocol)") // false
print("StubProtocol conformance of 'castedDeserializedDucks': \(castedDeserializedDucks is StubProtocol)") // true
Only when casting the value from Any
to [String]
the cast to the protocol will work. The same goes for the cast in the represent(:)
function:
if let representable = value as? NodeRepresentable
The casting would work only if the Any
value is properly casted. But then it will fail with this value children since the call is recursive (e.g. with an array or a dictionary).
Solution
Recursive cast
A solution that the developer can implement is a recursive cast of the Any
value to NodeRepresentable
. For instance, here is an implementation in Scout.
func castToNodeRepresentable(_ value: Any) -> NodeRepresentable {
if var value = value as? [String: Any] {
value.forEach { value[$0.key] = castToNodeRepresentable($0.value) }
return value
} else if let value = value as? [Any] {
return value.map(castToNodeRepresentable)
} else if let value = value as? Int {
return value as NodeRepresentable
} else if let value = value as? Double {
return value as NodeRepresentable
} else if let value = value as? Date {
return value as NodeRepresentable
} else if let value = value as? Bool {
return value as NodeRepresentable
} else {
return String(describing: value) as NodeRepresentable
}
}
This does work, especially as the serialisation handles a limited set of types.
But then it seems strange to have to cast an Any
value to a NodeRepresentable
if the function dump(object:)
expects Any
.
One solution could be for the library to perform this recursive cast on behalf of the developer. Thus making sure the provided Any
value conforms to the NodeRepresentable
protocol. But this approach has some weaknesses:
- performing this operation can take some time on large chunks of data
- it does not allow customisation of the process. For example the developer could prefer to throw an error rather than fall back on a
String
.
Requiring a NodeRepresentable
Another solution could be to change the dump(object:)
parameter type from Any?
to NodeRepresentable
- This would make the developer aware of the requirement rather than hiding it. Thus making them responsible for conforming
Any
to this protocol, while also allowing customisation. - Requiring a
NodeRepresentable
prevents a runtime error that can be misleading. After all, if therepresent(:)
function has to work with aNodeRepresentable
object, why wouldn't it ensure it rather than handling anAny
value? In fact, I first thought that the problem came from the YAMS library since I was passing it anAny
value and there were no requirements on this value. Then I realised the problem came from the deserialisation process of Foundation.
Please let me know if this solution seems valid. If so, I would open a pull request with the proposed modifications.