0

I am looking for a good way of persisting arbitrary subclasses.

I am writing objects asDictionary to json upon save, and init(json) them back upon load. The structure is Groups that have Units of different kind. The Group only knows its units as something implementing UnitProtocol.

Groups and Units

The subclasses UA etc of Unit has the exact same data as a Unit. So data wise, the asDictionary and init(json) fits well in Unit. The subclasses only differ in logic. So when restoring them from file, I believe it has to be the exact subclass that is initialized.

(Bad) Solutions I thought of

  1. Letting every group know that it can have Units of different subclasses by not holding them as [UnitProtocol] only, but also as [UA], [UB], etc, that can saved separately, restored by their respective sub inits, and be merged into a [UnitProtocol] upon init.
  2. Store subunits with their classname and create a Unit.Init(json) that somehow is able to pass down the initialization depending on subtype.
  3. ?? Still thinking, but I believe there has to be something I can learn here to do this in a maintainable way without breaking the single responsibility policy.
Community
  • 1
  • 1
JOG
  • 5,590
  • 7
  • 34
  • 54
  • Is there a reason you're avoiding `NSKeyedArchiver`, which handles all of this transparently? JSON seems a lot of work for no benefit if the only goal is to save and restore objects. – Rob Napier May 19 '17 at 20:10
  • Reason: Not knowing about it. How do I use it? If you mean "why json?" is because I like json when the data leaves iOS and goes cloudy. – JOG May 19 '17 at 20:11
  • See https://developer.apple.com/library/content/documentation/Cocoa/Conceptual/Archiving/Archiving.html#//apple_ref/doc/uid/10000047i – Rob Napier May 19 '17 at 20:11
  • 1
    Note that Swift 4 will have an even more powerful solution. See https://github.com/apple/swift-evolution/blob/master/proposals/0167-swift-encoders.md (but it won't be available for a couple of months or so) – Rob Napier May 19 '17 at 20:14
  • I don't know what you mean by "goes cloudy" here. Do you actually need to post it to a service that requires JSON? (iCloud is a "cloudy" service, and works entirely without JSON.) – Rob Napier May 19 '17 at 20:16
  • Aha yes, I have not started my server back end but will probably be .Net, node or go, and a doc db or graph db. Not sure how that would benefit from using iCloud? – JOG May 19 '17 at 20:22
  • I don't understand then; in your question, you say "restoring them from file." Is this designed to handle a file or an API? If it's a JSON API that you haven't written yet, then I would assume you would just add a type name to the JSON. If UA/UB/UC have exactly the same properties, then how would you possibly distinguish them in JSON? You would have to add something. – Rob Napier May 19 '17 at 20:30
  • Well, my question is about initializing arbitrary subclasses from json, and not about why I chose json. Currently this json is on file, and I design for it to also go to and from an API in the future. See my answer below for a solution that worked for me. I appreciate your comments and will look into them. – JOG May 19 '17 at 20:35

2 Answers2

1

For init class from json I used this technic :

        //For row in json
        for row in json {
            //namespace it use for init class
            let namespace = (Bundle.main.infoDictionary!["CFBundleExecutable"] as! String).replacingOccurrences(of: " ", with: "_")
            //create instance of your class
            if let myClass = NSClassFromString("\(namespace).\(row.name)") as? NameProtocol.Type{
                //init your class
                let classInit : NameProtocol = myClass.init(myArgument: "Hey")
                //classInit is up for use
            }
        }
  • Ok, so with this technique I would store the namespace and class name inside of the Unit json? – JOG May 19 '17 at 16:25
  • @JOG You need store only class name, for example : [ {"name": "MyClassA", "init": "Horus" }, {"name": "MyClassB", "init": "JOG" } ] – Horusdunord May 19 '17 at 16:34
  • Could you please explain the NameProtocol.Type part of your solution? – JOG May 19 '17 at 18:23
1

Store every Unit json with its classname

func asDictionary() -> Dictionary<String, Any> {
    let className = String(describing: type(of: self))

    let d = Dictionary<String, Any>(
        dictionaryLiteral:
        ("className", className),

        ("someUnitData", someUnitData),
        // and so on, for all the data of Unit...

And init from json with @horusdunord's solution:

for unitJson in unitsJson {

        let className = (unitJson as! [String: Any])["className"] as? String
        let namespace = (Bundle.main.infoDictionary!["CFBundleExecutable"] as! String).replacingOccurrences(of: " ", with: "_")
        if let unitSubclass = NSClassFromString("\(namespace).\(className ?? "N/A")") as? UnitProtocol.Type {

            if let unit = unitSubclass.init(json: unitJson as! [String : Any]) {
                units.append(unit)
            } else {
                return nil
            }
        } else {
            return nil
        }
    }

The trick here is that casting the class unitSubclass to UnitProtocol then allows you to call its init(json) declared in that protocol but implemented in the particular subclass, or in its superclass Unit, if the properties are all the same.

JOG
  • 5,590
  • 7
  • 34
  • 54
  • 1
    This is very, very dangerous. Never create classes base on data you received from an API. This is a very classic vulnerability. You should know what classes your system handles. Create a dictionary of JSON names to types. (If that's unclear, I'll write an example.) never call `NSClassFromString` on something you received over the network. – Rob Napier May 19 '17 at 20:32
  • Aha, you mean to check that the className is one of the ones I expect it to be? – JOG May 19 '17 at 20:40
  • 1
    Yes. Otherwise you can be handed something arbitrary that happens to respond to a method that you call. This is a really common exploit in the Java world (where deserializing arbitrary things from the network is common). – Rob Napier May 19 '17 at 20:46
  • Impressive catch. So, I am going to need something like this http://stackoverflow.com/a/42749141/237509 to list all the classes implementing my UnitProtocol, right? – JOG May 19 '17 at 20:51
  • No; nothing like that. Make a dictionary. `let classes = [ "Something": Something.Self, "Other": Other.Self]`. That way if you change the name of a class in your code, it won't break the JSON. – Rob Napier May 19 '17 at 21:02
  • Ok the only overhead would be to manually add to that dict for every subclass I'd make of any Protocol. – JOG May 19 '17 at 21:04
  • Just anything you actually can serialize (if this is massive, you should rethink your class hierarchy). But it's pretty critical as a security measure, and it makes your system extremely flexible in the future. – Rob Napier May 19 '17 at 21:18
  • 1
    (And there is a somewhat common pattern of using a "register" method to update the dictionary so that classes can register themselves. If they're `NSObject` subclasses, then you can put that in `+load` and it'll automatically run at the beginning of the program. But often this overkill; again, if it's incredibly complicated, you likely have too many classes.) – Rob Napier May 19 '17 at 21:20
  • Thanks for sharing your expertise! I learned quite some stuff. :) – JOG May 19 '17 at 21:27