iwill/generic-json-swift

How to use JSON in Core Data

Opened this issue · 7 comments

Core Data Interop

How should JSON properties be used in conjunction with Core Data?

Discussion

JSON neatly solves the case where a Swift type needs to be created from a freeform json string (i.e. the json schema is unknown at compile time and subject to change). Furthermore, it allows the json to modelled in a type safe way.

Codable conformance allows the type to be easily converted to and from Data. This makes persisting the type to disk as data relatively simple.

However using the JSON type in the context of Core Data poses challenges. JSON was designed as a pure Swift type whereas Core Data has is origin in Objective-C.

The issue is that using a property of type JSON on an NSManagedObject subclass is not permitted. The compiler produces an error:

class Person: NSManagedObject {
    
    @NSManaged var firstName: String
    @NSManaged var lastName: String
    @NSManaged var metaData: JSON // Property cannot be marked @NSManaged because its type cannot be represented in Objective-C
    
}

This is expected since Core Data does not support JSON out of the box, and it turns out, the metaData attribute cannot be marked as Transformable since Swift Enums with associated values (which JSON is built upon) cannot be represented in Objective-C and so conformance to NSCoding seems unlikely.

By contrast Swift dictionaries, say of type [String: Any] can be used on an NSManagedObject subclass directly since they conform to NSCoding and so their corresponding model attribute can be marked as Transformable.

Options

How then should properties of type JSON be used in the context of Core Data? Or, does Core Data's tight integration with Objective-C mean the JSON type cannot be used?

Some options which come to mind:

  1. Add NSCoding conformance to JSON allowing the attribute's type to be Transformable.
    • This does not seem technically possible as mentioned above.
  2. Use a custom ValueTransformer.
    • This is not possible because JSON does not derive from NSObject.
  3. Add a toDictionary() and fromDictionary methods facilitating [String: Any] transformations.
    • The attribute type will be Transformable with a custom setter and getter in the NSManagedObject's subclass to do the transformation.
  • This seems a bit heavy handed. The transformation cost will be paid on each get or set.

Are they other ways to do this? (I am relatively new to Core Data).

Example Object

class Person: NSManagedObject {
    
    @NSManaged var firstName: String
    @NSManaged var lastName: String
    @NSManaged var metaData: JSON // Property cannot be marked @NSManaged because its type cannot be represented in Objective-C
    
}
let personAData: Data = """
    {
      "firstName": "John",
      "lastName": "Appleseed",
      "metaData": {
        "foo": "bar",
        "rah": [1, 2, 3]
      }
    }
    """.data(using: .utf8)!

let personBData: Data = """
    {
      "firstName": "Joe",
      "lastName": "Lemon",
      "metaData": {
        "yaz": {
          "tar": 1234.00
        }
      }
    }
    """.data(using: .utf8)!

Here the metaData property needs to hold and store flexible json which makes JSON a good candidate.

zoul commented

To be honest, I have never used Core Data. This feels like one of the Swift ↔︎ Objective-C interoperability pain points. Would it be acceptable for you to introduce a second, serializable version of the problematic property?

class Person: NSManagedObject {
    var metaData: JSON?
    @NSManaged private var encodedMetaData: Data?
}

And then you could keep those two in sync using didSet or custom accessors. (You can’t use the @NSManaged annotation in that case AFAIK: @NSManaged not allowed on observing properties, but that seems to be solvable.) I don’t know how acceptable the resulting performance is for you.

just to say I'm interested in this issue as well. My current hack is to store my JSON as a String in core data. Then do a conversion like this back to generic JSON:

    extension String {
         func toGenericJSON() -> JSON? {
           guard let data = self.data(using: .utf8, allowLossyConversion: false) else { return nil }
           return try? JSONDecoder().decode(JSON.self, from: data)
       }
}

Personally I’d use an ObjC_JSON class that would inherit from NSObject and conform to NSCoding, and would have a single property, the underlying JSON object.
You could then have a computed property in an extension of your model to access the underlying JSON directly (custom getter and setter that would get and set the objcJSON property of the model).

In my specific case, I have a lot of different types of JSON objects (sometimes of unknown structure). So I do not want to again build a large library of classes to convert json to object class and then back again to json. It is much easier for me to use GenericJSON and keep the objects as JSON. So I was looking for a way to store that in core data. One way is to use Data type, at the moment I just use String as a temporary fix. I was looking for a better more principled way to store JSON in core data.

I stand by my comment… The ObjC_JSON class I would create would contain a (Generic)JSON object. The NSCoding implementation of the class would simply create a JSONEncoder() and store the encoded JSON to encode.

If you’re not sure what I mean, I might be able to provide you w/ a code example.

yes please, it would be great if you could provide some code. Note I know nothing about Objective-c.