Yams.dump function Any object parameter casted as NodeRepresentable fails
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
Anyto this protocol, while also allowing customisation. - Requiring a
NodeRepresentableprevents a runtime error that can be misleading. After all, if therepresent(:)function has to work with aNodeRepresentableobject, why wouldn't it ensure it rather than handling anAnyvalue? In fact, I first thought that the problem came from the YAMS library since I was passing it anAnyvalue 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.
Regarding the conformance of the deserialised Any, I filed a bug on bugs.swift.org.
Thanks for filing that! I think your proposed solution makes sense as well.
Thank you! Someone answered to my issue. That's not a bug in fact.
The thing is, with deserialisation, the Any value is a NSArray and not an Array. Which is why I think it will not conform to NodeRepresentable since the conformance is only declared on the Array type in the library.
A quick fix would be to also add the conformance of NSArray to NodeRepresentable but I would say it's clearer to require a NodeRepresentable value in the function as mentioned.