Realm-cocoa: RealmSwift.Object doesn't work with Swift 4's Codable

Created on 9 Jun 2017  路  13Comments  路  Source: realm/realm-cocoa

Goals

I'm trying to remove ObjectMapper and use Swift 4's new Codable interface to encoding/decoding
Realm Object to/from JSON.

Expected Results

Encoding/Decoding should work.

Actual Results

I get a swift compiler error:

<unknown>:0: error: super.init isn't called on all paths before returning from initializer

It seems there is an issue with Decodable interface, so if I add:
required convenience init(from decoder: Decoder) throws { self.init() }

But if I do this, encoding will work, but Decoding will fail (no decoding is actually done) as I'm providing my own init method, which will override the init method synthesized by the swift compiler.

Steps to Reproduce


See code sample.

Code Sample

import Foundation
import RealmSwift
import Realm

class LoginArgs: RealmSwift.Object, Codable {
    @objc dynamic var userName : String = ""
    @objc dynamic var password : String = ""
    @objc dynamic var pushToken : String = ""
    @objc dynamic var userAgent : String!
    @objc dynamic var version : String!

    private enum CodingKeys: String, CodingKey {
        case userName  = "UserName"
        case password  = "Password"
        case pushToken = "PushNotificationToken"
        case userAgent = "UserAgent"
        case version   = "Version"
    }

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

    convenience init(_ userName:String, _ password:String, pushToken:String = "DUMMY", userAgent:String! = nil, version:String! = nil) {
        self.init()
        self.userName = userName
        self.password = password
        self.pushToken = pushToken
        self.userAgent = userAgent
        self.version = version
    }   
}

func test() {
    let args = LoginArgs(username, password, pushToken: "DUMMY", version: "1.2")
        let jsonEncoder = JSONEncoder()
        let json = try! jsonEncoder.encode(args)
        print(String(data: json, encoding: .utf8)!)

        let jsonDecoder = JSONDecoder()
        let object = try! jsonDecoder.decode(LoginArgs.self, from: json)
        print(object)
}

test()
/* Outputs:
{"Version":"1.2","UserName":"acmobile","Password":"ng1ng1","PushNotificationToken":"DUMMY"}
LoginArgs {
    userName = ;
    password = ;
    pushToken = ;
    userAgent = (null);
    version = (null);
}
*/

Version of Realm and Tooling


Realm framework version: "master" (for Swift 4/Xcode 9 support)

Realm Object Server version: N/A

Xcode version: XCode 9 Beta

iOS/OSX version: iOS 10.3.1

Dependency manager + version: ? Carthage 0.23.0

T-Help

Most helpful comment

@kashifshaikh Probably you need to implement init(from decoder: Decoder) method in your type instead of using the default implementation:

class LoginArgs: RealmSwift.Object, Codable {
    @objc dynamic var userName : String = ""
    @objc dynamic var password : String = ""
    @objc dynamic var pushToken : String = ""
    @objc dynamic var userAgent : String!
    @objc dynamic var version : String!

    private enum CodingKeys: String, CodingKey {
        case userName  = "UserName"
        case password  = "Password"
        case pushToken = "PushNotificationToken"
        case userAgent = "UserAgent"
        case version   = "Version"
    }

    required convenience init(from decoder: Decoder) throws {
        self.init()
        let container = try decoder.container(keyedBy: CodingKeys.self)
        userName = try container.decode(String.self, forKey: .userName)
        password = try container.decode(String.self, forKey: .password)
        pushToken = try container.decode(String.self, forKey: .pushToken)
        userAgent = try container.decodeIfPresent(String.self, forKey: .userAgent)
        version = try container.decodeIfPresent(String.self, forKey: .version)
    }

    convenience init(_ userName: String, _ password: String, pushToken: String = "DUMMY", userAgent: String! = nil, version: String! = nil) {
        self.init()
        self.userName = userName
        self.password = password
        self.pushToken = pushToken
        self.userAgent = userAgent
        self.version = version
    }
}

Results:

{"Version":"1.2","UserName":"Katsumi","Password":"password","PushNotificationToken":"DUMMY"}
LoginArgs {
    userName = Katsumi;
    password = password;
    pushToken = DUMMY;
    userAgent = (null);
    version = 1.2;
}

All 13 comments

This sounds like a bug in the compiler. Either the auto-generated initializer should be calling super.init() or if auto-implementation of Codable isn't supported on types which inherit from a type with a required initializer then the compiler should be producing a more appropriate error message. I would recommend filing a radar or Swift bug.

Unrelated:
@kashifshaikh How are you getting Realm Swift to work with Xcode 9 beta (Swift 3.2/4) ? Downloading the Framework manually won't do it for me and Cocoapods won't work either.

There was an existing similar bug around required initializers with Codable. I've added my results to that here: https://bugs.swift.org/browse/SR-5122

Since this is an issue with the Swift compiler and toolchain, I'm going to close this ticket. Feel free to re-open it if SR-5122 is addressed but Realm still doesn't work as expected.

@kashifshaikh Probably you need to implement init(from decoder: Decoder) method in your type instead of using the default implementation:

class LoginArgs: RealmSwift.Object, Codable {
    @objc dynamic var userName : String = ""
    @objc dynamic var password : String = ""
    @objc dynamic var pushToken : String = ""
    @objc dynamic var userAgent : String!
    @objc dynamic var version : String!

    private enum CodingKeys: String, CodingKey {
        case userName  = "UserName"
        case password  = "Password"
        case pushToken = "PushNotificationToken"
        case userAgent = "UserAgent"
        case version   = "Version"
    }

    required convenience init(from decoder: Decoder) throws {
        self.init()
        let container = try decoder.container(keyedBy: CodingKeys.self)
        userName = try container.decode(String.self, forKey: .userName)
        password = try container.decode(String.self, forKey: .password)
        pushToken = try container.decode(String.self, forKey: .pushToken)
        userAgent = try container.decodeIfPresent(String.self, forKey: .userAgent)
        version = try container.decodeIfPresent(String.self, forKey: .version)
    }

    convenience init(_ userName: String, _ password: String, pushToken: String = "DUMMY", userAgent: String! = nil, version: String! = nil) {
        self.init()
        self.userName = userName
        self.password = password
        self.pushToken = pushToken
        self.userAgent = userAgent
        self.version = version
    }
}

Results:

{"Version":"1.2","UserName":"Katsumi","Password":"password","PushNotificationToken":"DUMMY"}
LoginArgs {
    userName = Katsumi;
    password = password;
    pushToken = DUMMY;
    userAgent = (null);
    version = 1.2;
}

I wrote a 'ReamSwift' Codable extension that I am working on.
Haven't fully tested everything yet, but this might make it easier to create 'Codable' RealmSwift models WITHOUT any custom init(from decoder: Decoder)

I stole most of this logic from the Optional and Array implementations in the Swift stdlib.
https://gist.github.com/mishagray/3ee82a3a82f357bfbf8ff3b3d9eca5cd

I'll update it if I find any issues during tests...

This looks great @mishagray! I really don't want a bunch of boilerplate code on each Realm.Object I have. Or I am keeping ObjectMapper in for a little while longer. It's especially annoying because I cannot implement init from Decodable outside of the class definition.. :/

@mishagray This RealmOptional implementation actually has a problem. Because in RealmSwift, optionals are defined as non-optional properties, Codable will fail if the value is not present, even if defined as RealmOptional.

@Legoless Ah.. I think that could be fixed, if I change the implementation of the encode/decode implementations...

That IS WEIRD though... cause I basically 'ported' RealmOptional's implementation from the built in 'Optional': https://github.com/apple/swift/blob/2e5817ebe15b8c2fc2459e08c1d462053cbb9a99/stdlib/public/core/Codable.swift

@mishagray Do you have any update on your gist? I tried to use it, but I am getting Use of unresolved identifier 'T' error :/

So I have a 'fork' of Realm that I created that synthesizes Codeable for Realm better than this gist.

But I haven't open a full PR with Realm for it yet...

You can use it via Cocoapods with:

    pod 'Realm', :git => 'https://github.com/FutureKit/realm-cocoa.git', :branch => 'mg/CodableAndRealmCustom3.0.2', :submodules => true
    pod 'RealmSwift', :git => 'https://github.com/FutureKit/realm-cocoa.git', :branch => 'mg/CodableAndRealmCustom3.0.2', :submodules => true

The full differences between this and 3.0.2 can be seen here:
https://github.com/realm/realm-cocoa/compare/v3.0.2...FutureKit:mg/CodableAndRealmCustom3.0.2

I needed to modify a few internal methods and I created a RealmOptionalProtocol to help 'map' the Swift type to internal Realm type slightly differently than it was done before.

It also has this other idea I was playing around with which is 'RealmCustom' which allows you to define a 'mapping' function between any Swift Type and any of the built in supported types, and it basically 'translates' the value. Works a lot like 'RealmOptional', (they share a common base class)... It works with Swift but if you use [] accessor in Swift e.g.let t = object['customProperty'], you will still get the 'internal' type, not the T type.

I've been meaning to clean this up and make a real PR ... but I have two features in this branch (Codable and my RealmCustom<>) so it should really be two PRs... And I of course (like every other lazy programmer) haven't written enough test cases for both of these features. So if anyone wants to do so... that would be cool. But the Codeable stuff does somewhat rely on the RealmOptional/RealmCustom refactor. I simplified the

One thing that IS kind of annoying about Codeable and Realm, is that by default, the Swift compiler doesn't want to assign default values correctly, for when the JSON is not present. This isn't a Realm issue, this is inherit in the Codeable implementation in the swift 4 compiler. The 'only' way to decode JSON with 'missing' properties, is to define them as Optional (or RealmOptional in this branch).

For example:
You want define a property

    public class Model: RealmSwift.Object, Codable {
        @objc dynamic public var id: Int = 0
        @objc dynamic public var contentLength: Int = 0
   }

But the server sends you a JSON { "id": 1234 }

The swift synthesized 'Codeable' will throw an exception here when you call 'try decoder.decode(Model.self, from: data)``` using the above JSON. It's not 'smart' enough to set contentLength to 0. If you define things as

    public class Model: RealmSwift.Object, Codable {
        @objc dynamic public var id: Int = 0
       let contentLength = RealmOptional<Int>()
   }

Now decode works, BUT you may not want to have an 'RealmOptional' in your model, if you want to support default values. You COULD write your own init(from decoder: Decoder) method to deal with this, but of course if you want to custom handle ONE key, you have to custom decode all of them. Which sort of defeats the whole coolness of Codable. It works BEST when you DON'T have to write your own custom decoder methods.

So... The way I solved this was creating a new class that inherits from JSONDecoder, (that I call) 'DefaultingJSONDecoder' and a new 'CodingKey' protocol called 'CodingKeyWithDefaults'.

So now I can define my type this way:

    public class Model: RealmSwift.Object, Codable {
        @objc dynamic public var id: Int = 0
        @objc dynamic public var contentLength: Int = 100

        public enum CodingKeys: String, CodingKeyWithDefaults {
            case id
            case contentLength

            // you only need to create this method, if you have weird custom Types that aren't Codeable, or you aren't satisified with a default value of 'zero' for the numeric Swift types.
            public func defaultValue<T>(_ type: T.Type) throws -> T? {
                switch self {
                case .contentLength where T.self is Int.Type:
                    return (Int(100) as! T) // swiftlint:disable:this force_cast
                default:
                    return self.defaultZeroValuesForNumberTypes(type)
                }
            }

        }
   }

And then I use the 'DefaultingJSONDecoder()'

let decoder: JSONDecoder = DefaultingJSONDecoder()
let t = try decoder.decode(Model.self, from: data)

By default, DefaultingJSONDecoder will decode 'Int' values to 0. And decode contentLength to 100 when it's missing.
If you want a 'custom' default value you need to define your own public func defaultValue<T>(_ type: T.Type) throws -> T? method. If you don't supply one, than it will set all the built in Int/Bool/Float/Double types to '0' .

You can grab THAT over here: https://gist.github.com/mishagray/9a595806a15dca07ad6a9fd5867bf0ff

Which I have also been wanting to publish as it's own Open Source library, since it's useful outside of Realm.

Thanks @mishagray :) So it seems quite complicated to use Codable with Realm for now, I hope they will enable it soon...

Was this page helpful?
0 / 5 - 0 ratings