A question from Swift novice: is there a mechanism to instantiate objects from String through reflection?
In Java, you could find a constructor or a static method returning the desired type and use it to construct objects. Is there a similar mechanism in Swift or do I have to implement it for all supported known-to-me types that I want to support in a long series of if statements?
Swift JSON decoder can do this but I didn't find the source to learn how. Any pointers in that direction?
Swift does not have general-purpose read/write reflection the way languages like Java provide. Some types allow round-tripping through regular strings via the LosslessStringConvertible
protocol, but things like JSON usually use the Codable
interfaces instead. Encodable
and Decodable
are very easy for models to implement -- and in many cases the implementation is automatic -- but writing a custom Encoder
and/or Decoder
is much more challenging.
It works for classes via Obj-C API (that works for Swift classes):
protocol Inittable { init() }
final class TestClass: Inittable {}
let name = NSStringFromClass(TestClass.self)
print(name)
let c: AnyClass = NSClassFromString(name)!
let theClass = c as! any Inittable.Type
let instance = theClass.init()
print(instance)
It also works via a manual registration:
protocol Inittable { init() }
var registeredTypes: [String: Inittable.Type] = [:]
func registerType(_ type: Inittable.Type) {
registeredTypes["\(type)"] = type
}
func findType(_ name: String) -> Inittable.Type? {
registeredTypes[name]
}
struct Test: Inittable {}
// register type
registerType(Test.self)
// lookup type
let instance = findType("Test")!.init()
print(instance)
Thanks!
I guess, I am partially satisfied with the manual implementation for types I need to support but I'll try to familiarise myself with LosslessStringConvertible
protocol, it seems that it could provide a more robust solution.
I am sure this limitation is a Swift design goal but it is hard to go back from the language features you relied upon. Overall, I am impressed with Swift's features, some of them are extremely helpful and desirable for me who has done mostly Java development in the last 20 years
Note that JSON decoder deals with a very limited number of bottom types (strings, bools, ints, floats and arrays and dictionaries of those). Also on the decoding side you are explicitly passing the type to decode, so it doesn't have to find a type by string or anything like that. NSKeyedArchiver sounds closer in nature to what this question is about but it's for NSObjects only.
Worth noting that rarely instantiating a type with init()
will be enough, you'd need to somehow customise the instance (via parameters in init or setters). If you provide an example / pseudocode of what you are trying to achieve we could help further. Are you looking for something like JSON but with the type information embedded? e.g. this?
let value = ... // anything really
let data = SomethingLikeJsonEncoder(value)
let value2 = SomethingLikeJsonDecoder(data)
print(value2) // same as `value` above
This is doable
var registeredTypes: [String: Codable.Type] = [:]
func registerType(_ type: Codable.Type) {
registeredTypes["\(type)"] = type
}
func findType(_ name: String) -> Codable.Type? {
registeredTypes[name]
}
struct Container: Codable {
var typeName: String; var subData: Data
}
extension JSONEncoder {
func encode2(_ v: Encodable) throws -> Data {
let subData = try JSONEncoder().encode(v)
let container = Container(typeName: "\(type(of: v))", subData: subData)
return try JSONEncoder().encode(container)
}
}
extension JSONDecoder {
func decode2(_ data: Data) throws -> Decodable {
let container = try decode(Container.self, from: data)
return try decode(findType(container.typeName)!, from: container.subData)
}
}
Usage example
struct S: Codable {
var x = 123
var y = 456
}
registerType(S.self)
let value = S()
let data = try! JSONEncoder().encode2(value)
print(String(data: data, encoding: .utf8)!)
// "{"subData":"eyJ4IjoxMjMsInkiOjQ1Nn0=","typeName":"S"}
let value2 = try! JSONDecoder().decode2(data)
print(value2)
let value3 = value2 as! S
print(value3)
The question is what do you plan to do with value2
– it is of type Any
... You could cast it to a proper type, like in the example, but that begs the question: why marshalling through Any instead of using this known type during decoding?
I need to parse JSON with a partially known structure, meaning that a field type determines at RT how to parse the rest of the document without knowing anything about its nature. As far as I understand I can't use JSONDecoder
as I have no struct definition to pass. I am writing a manual parser that traverses a dictionary created by JSONSerialization
(BTW, is it the right conclusion?)
To simplify the process I implemented a utility as such:
public func convertToOptionalObject<T> (_ jsonDict: [String : Any], _ name: String ) throws -> T? {
let value = jsonDict[name]
if (value == nil) {
return nil
}
let result = value as? T
if (result != nil) {
return result
}
// Try to construct from String
let optTypedString : String? = value as? String
if (optTypedString == nil) {
throw JsonParsingError.invalid (type: String(describing: T.self), name: name, value: String(describing: value!))
}
//TODO: Add BOOL and Float/Double
let typedString = optTypedString!
if (T.self == Int.self) {
let ival = Int( typedString)
if (ival != nil) {
return ival as! T?
}
}
if (T.self == UUID.self) {
let uuid = UUID(uuidString: typedString)
if (uuid != nil) {
return uuid as! T?
}
}
throw JsonParsingError.invalid (type: String(describing: T.self), name: name, value: String(describing: value!))
}
My desire was to support any type constructible from String (as I would do in Java) but apparently, I can't. Does this solution seem reasonable?
:( I started using Swift a few weeks ago and don't feel if something I am doing makes sense for this language. Any points are truly appreciated
If the structure is unknown JSONSerialization is a good choice.
JSONSerialization.jsonObject(with: data)
gives you Any
and to "parse" that Any
I'd use something like this:
func parse(_ value: Any?, level: Int = 0) {
func indentation(_ level: Int) -> String {
String(repeating: " ", count: level)
}
let indent = indentation(level)
guard let value else {
print(indent, "nil")
return
}
if let array = value as? [Any] {
print(indent, "array of \(array.count) items")
array.enumerated().forEach { index, item in
print(indentation(level + 1), "\(index)")
parse(item, level: level + 1)
}
} else if let dictionary = value as? [String: Any] {
print(indent, "dictionary of \(dictionary.count) items")
dictionary.forEach { key, value in
print(indentation(level + 1), "key: \(key)")
parse(value, level: level + 1)
}
} else if let v = value as? Int {
print(indent, "int: \(v)")
} else if let v = value as? Double {
print(indent, "double: \(v)")
} else if let v = value as? Bool {
print(indent, "bool: \(v)")
} else if let v = value as? String {
print(indent, "string: \(v)")
} else {
fatalError("TODO")
}
}
As it is JSON you are only dealing with a limited number of types here, it's hardly "a long list of if statements".
As it is JSON you are only dealing with a limited number of types here, it's hardly "a long list of if statements
I want to support more than what standard JSON does(e.g units for integers).
"duration" : "5 years"
that should be converted to msec as Int
And what specifically should be my choice based on application needs. I tried to avoid tedious conversion but hoped I would not need to write special code for each case as I did in the example above for Int
and UUID
but use reflection. So far I don't see how to do that but granted that the number of types to be supported is small it should not be a big deal.
Thanks for the help
This is very app specific. e.g. would you support "5 years and 6 months", " 5 years ", "1 year" v "2 years", "5 Years", "5 années", etc. Then you'd probably want to do this for some fields (like "duration") but not others (like "userComment").
IRT to not using a series of "if"s: consider this approach (simplifying your code):
protocol InittableWithString {
init?(_ string: String)
}
extension Int: InittableWithString {
// no need to implement anything here as Int has appropriate init already
}
extension UUID: InittableWithString {
// we need to implement this init as UUID's init has a different signature
init?(_ string: String) {
self.init(uuidString: string)
}
}
func convertToOptionalObject<T: InittableWithString>(_ string: String) -> T? {
T.init(string)
}
let x: Int? = convertToOptionalObject("1")
print(x) // Optional(1)
let y: UUID? = convertToOptionalObject(UUID().uuidString)
print(y) // Optional(15A1353E-9A8F-40BD-97B8-B40C85A08821)
You are right, there are always limits - I don't think it makes sense to support 5 years and 6 months
. This could be naturally expressed as 17 months. What is likely that nobody needs 5 years 3 months and 100 msec
. It is possible but not very likely, but converting 5 years in msec is a pretty unpleasant task for configuration purposes (both calculation and readability). On the other hand (and my experience doing this type of work before) it helps hugely because usually periods are in a relatively narrow range of units. The same idea applies not only to time intervals but to KB, MB, KiB and MiB, etc.
I have to think of your protocol idea, it makes perfect sense if the same mechanism could be used for different parsing not specifically JSON/dictionary