I'm trying to remove ObjectMapper and use Swift 4's new Codable interface to encoding/decoding
Realm Object to/from JSON.
Encoding/Decoding should work.
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.
See 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);
}
*/
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
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.
@mayurdzk see my comment in https://github.com/realm/realm-cocoa/issues/5021
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 'RealmCustomlet 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...
Most helpful comment
@kashifshaikh Probably you need to implement
init(from decoder: Decoder)method in your type instead of using the default implementation:Results: