How to make the RealmSwift RealmOptional compatible with Swift Codable?
Asked Answered
K

5

8

Im facing an issue where I can't make the RealmOptional compatible with swift new Codable feature with json decoder.

Cosider the following Realm object.

class School: Object, Codable {

    @objc dynamic var id: Int64 = 0

    @objc dynamic var name: String?
    var numberOfStudents = RealmOptional<Int64>()
    var classes = List<Class>()

    enum CodingKeys: String, CodingKey {
       case id
       case name
       case numberOfStudents
       case classes
    }
}

class Class: Object, Codable {
    var name: String?
    var numberOfStudents = RealmOptional<Int64>()
}

Here we can declare the class as Codable because I wrote an extension for RealmOptinal with the help of this gist. But the problem is when the decoder decodes the json.

Consider this json

let jsonData = """
[
    "id": 1234,
    "name": "Shreesha",
    "numberOfStudents": nil,
    "classes": {
       "name": "Class V",
       "numberOfStudents": 12
    }
]
""".data(using: .utf8)!

In this json all the data are passed and this decodes perfectly with the code.

let decoder = JSONDecoder()

let decoded = try! decoder.decode(School.self, from: jsonData)

But if I remove the numberOfStudents key from the json data which supposed to be a RealmOptional object it will throw an error and it will not decode because RealmOptional is not a swift optional so the decoder thinks that there should be a key in the json data. In JSONDecoder it doesn't try to decode if the key is not there in the json and the property is declared as optional. It simply skips to other keys.

Until now I didn't override the initialiser because we had all the supporting extensions for RealmOptional Realm Lists etc. But now I have to override the init(from decoder: Decoder) to decode it manually and the Realm model has more than 50 properties in it (You know what I mean).

If we override the initialiser I feel there is not point in using JSONDecoder because there is more manual work than using JSONDecoder.

required convenience init(from decoder: Decoder) throws {
    self.init()
    let container = try decoder.container(keyedBy: CodingKeys.self)

    id = try container.decodeIfPresent(Int64.self, forKey: .id) ?? 0
    name = try container.decodeIfPresent(String?.self, forKey: .name) ?? ""
    numberOfStudents = try container.decodeIfPresent(RealmOptional<Int64>.self, forKey: .numberOfStudents) ?? RealmOptional<Int64>()

    let classesArray = try container.decode([Class].self, forKey: .classes)
    classes.append(objectsIn: classesArray)
}

So can someone suggest me the alternate solution to make the RealmOptional compatible with JSONDecoder so that we don't have to override the initialisers.

Kaon answered 12/7, 2018 at 9:31 Comment(0)
F
7

Here is something you can do to work around the problem. Create a new class which supports decoding and has RealmOptional as its property.

class OptionalInt64: Object, Decodable {
    private var numeric = RealmOptional<Int64>()

    required public convenience init(from decoder: Decoder) throws {
        self.init()

        let singleValueContainer = try decoder.singleValueContainer()
        if singleValueContainer.decodeNil() == false {
            let value = try singleValueContainer.decode(Int64.self)
            numeric = RealmOptional(value)
        }
    }

    var value: Int64? {
        return numeric.value
    }

    var zeroOrValue: Int64 {
        return numeric.value ?? 0
    }
}

Then, instead of using RealmOptional in your school class, use this new OptionalInt64 class,

class School: Object, Codable {

    @objc dynamic var id: Int64 = 0

    @objc dynamic var name: String?
    @objc dynamic  var numberOfStudents: OptionalInt64?
    var classes = List<Class>()

    enum CodingKeys: String, CodingKey {
       case id
       case name
       case numberOfStudents
       case classes
    }
}

Note that now instead of using RealmOptional, you are using RealmNumeric? which is of type Optional. Since, it is optional, automatic decoding uses decodeIfPresent method to decode the optional value. And if it is not present in json the value will simply become nil.

Freeswimming answered 3/9, 2018 at 19:21 Comment(0)
U
2

I have modified the solution of Sandeep to be more generic:

class RealmOptionalCodable<Value: Codable>: Object, Codable where Value: RealmSwift.RealmOptionalType {

    private var numeric = RealmOptional<Value>()

    var value: Value? {
        get {
            numeric.value
        }
        set {
            numeric.value = newValue
        }
    }


    required public convenience init(from decoder: Decoder) throws {
        self.init()

        let singleValueContainer = try decoder.singleValueContainer()
        if singleValueContainer.decodeNil() == false {
            let value = try singleValueContainer.decode(Value.self)
            numeric = RealmOptional(value)
        }
    }

}

Using

@objc dynamic  var numberOfStudents: RealmOptionalCodable<Int>?
Ugrian answered 12/1, 2020 at 12:21 Comment(2)
How do we assign value? wr.integer = RealmOptionalCodable(value: 21) crashesAllergy
You must assign like this: numberOfStudents.value = 21Ugrian
T
1
  1. Add @objcMembers above your Realm Model class.

  2. Use variable as below

public dynamic var someValue = RealmOptional<Int>()
  1. While assigning values to realm optional, you can use someValue.value = 10

By default someValue will be nil.

Torpedo answered 14/3, 2019 at 11:22 Comment(0)
P
0

I found this solution and it works like a charm. I am using the updated code from srv7's comment.

Progestational answered 9/8, 2018 at 17:52 Comment(2)
I already have this code in my codebase. I just wanted to use RealmOptional without overriding the initializer. Because as RealmOptional is not swift optional JSONDecoder thinks that there should be a key-value pair in the json and tries to decode the code. That's why it throws an error. I want the solution to this. Thanks.Kaon
While this link may answer the question, it is better to include the essential parts of the answer here and provide the link for reference. Link-only answers can become invalid if the linked page changes.Phelia
D
0

Since last year, Realm added a new and more easier way for optionals, using @Persisted - docs

How to use it:

class Order: Object, Codable {

   @Persisted(primaryKey: true) var productOrderId: Int?

   @Persisted var name: String?
   @Persisted var standardPrice: Double?
   @Persisted var paid: Bool?

}
Dion answered 9/2, 2022 at 14:52 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.