I'm facing a very weird issue where calling Mapper().toJSON(object)
modifies object
's properties.
I noticed this because I also use this object with realm, and modifying an object outside a transaction will crash the app.
To reproduce, you can create a new single view project in XCode, add ObjectMapper
as a pod and Realm
as a framework manually. Then create a User
model that is a RLMObject
and that conforms to MapperProtocol
. Create a user, insert it in the realm, then try to convert it to JSON outside of the write transaction.
import Foundation
import Realm
import ObjectMapper
class User: RLMObject, MapperProtocol {
dynamic var name: String?
override required init!() {
super.init()
}
func map(mapper: Mapper) {
name <= mapper["name"]
}
}
import UIKit
import Realm
import ObjectMapper
class ViewController: UIViewController {
override func viewDidLoad() {
super.viewDidLoad()
var user = User()
user.name = "bla"
let realm = RLMRealm.defaultRealm()
realm.beginWriteTransaction()
realm.addObject(user)
realm.commitWriteTransaction()
let dictionary = Mapper().toJSON(user) // Crashes on this line
}
}
To make things easier, here's the full project: https://s3.amazonaws.com/uploads.hipchat.com/13599/245679/bPBl5yiijSuxJBX/TestObjectMapping.zip
Make sure you have an All exceptions
breakpoint set up.
2015-01-20 21:47:06.061 TestObjectMapping[14330:1161838] *** Terminating app due to uncaught exception 'RLMException', reason: 'Attempting to modify object outside of a write transaction - call beginWriteTransaction on an RLMRealm instance first.'
*** First throw call stack:
(
0 CoreFoundation 0x00000001106d7b75 __exceptionPreprocess + 165
1 libobjc.A.dylib 0x000000011225fbb7 objc_exception_throw + 45
2 TestObjectMapping 0x000000010fd53a85 _ZL11RLMSetValueP9RLMObjectmP11objc_object + 1285
3 TestObjectMapping 0x000000010fd5583b ___ZL13RLMMakeSetterIU8__strongP11objc_objectS2_EPFvvEmb_block_invoke_2 + 19
4 TestObjectMapping 0x000000010fd4ee2e _TFC17TestObjectMapping4User3mapfS0_FC12ObjectMapper6MapperT_ + 1374
5 TestObjectMapping 0x000000010fd4f1fe _TTWC17TestObjectMapping4User12ObjectMapper14MapperProtocolFS2_3mapUS2___fRQPS2_FCS1_6MapperT_ + 78
6 ObjectMapper 0x0000000110061d31 _TFC12ObjectMapper6Mapper6toJSONfS0_US_14MapperProtocol__FQ_GVSs10DictionarySSPSs9AnyObject__ + 561
7 TestObjectMapping 0x000000010fd4a07f _TFC17TestObjectMapping14ViewController11viewDidLoadfS0_FT_T_ + 2703
8 TestObjectMapping 0x000000010fd4a172 _TToFC17TestObjectMapping14ViewController11viewDidLoadfS0_FT_T_ + 34
9 UIKit 0x000000011108bf10 -[UIViewController loadViewIfRequired] + 738
10 UIKit 0x000000011108c10e -[UIViewController view] + 27
11 UIKit 0x0000000110faaeb9 -[UIWindow addRootViewControllerViewIfPossible] + 58
12 UIKit 0x0000000110fab251 -[UIWindow _setHidden:forced:] + 247
13 UIKit 0x0000000110fb793c -[UIWindow makeKeyAndVisible] + 42
14 UIKit 0x0000000110f61c01 -[UIApplication _callInitializationDelegatesForMainScene:transitionContext:] + 2732
15 UIKit 0x0000000110f649a3 -[UIApplication _runWithMainScene:transitionContext:completion:] + 1349
16 UIKit 0x0000000110f63875 -[UIApplication workspaceDidEndTransaction:] + 179
17 FrontBoardServices 0x000000011751a253 __31-[FBSSerialQueue performAsync:]_block_invoke + 16
18 CoreFoundation 0x000000011060c9bc __CFRUNLOOP_IS_CALLING_OUT_TO_A_BLOCK__ + 12
19 CoreFoundation 0x0000000110602705 __CFRunLoopDoBlocks + 341
20 CoreFoundation 0x0000000110601ec3 __CFRunLoopRun + 851
21 CoreFoundation 0x0000000110601906 CFRunLoopRunSpecific + 470
22 UIKit 0x0000000110f632e2 -[UIApplication _run] + 413
23 UIKit 0x0000000110f660e0 UIApplicationMain + 1282
24 TestObjectMapping 0x000000010fd4fe1e top_level_code + 78
25 TestObjectMapping 0x000000010fd4fe5a main + 42
26 libdyld.dylib 0x00000001132cc145 start + 1
)
libc++abi.dylib: terminating with uncaught exception of type NSException
I tried to explore the operator overloading jungle but couldn't find where ObjectMapper
modifies the object. I'm really surprised that it's even a thing, serializing should not do that.
Any idea of what's going on?
Hey there,
Weird issue indeed. It took me a little while to track down...
The custom operator that ObjectMapper uses is used for both Serialization and Deserialization of JSON. In deserialization the object on the left of the operator is being modified but not in serialization. Because of this the object being passed on the left side of the operator gets the flag "inout" in the operator function definition. This tells the compiler that the variable is modifiable within the scope of the function. This seems to be what Realm is unhappy about even though the object is not being modified during serialization. When I removed the flag and the deserialization code, the crash no longer happens...
I can't think of a solution off the top of my head to solve this issue. I will put some more thought into it to see if I can come up with a solution.
Tristan
Hi @tristanhimmelman, I am having the exactly same issue in my project.
During serialization of my Realm objects the App crashes as Realm thinks I am modifying the object when the mapping function is called.
As the issue is closed I guess you've found a solution for this problem?
I am using Xcode 6.3 beta with Swift 1.2, so I am on the "swift-1.2" branch, maybe the fix is not on it?
Enzo
Hi @zocario, no unfortunately I have not come up with a solution to this problem yet.
Did you try to talk with Realm guys to find a solution?
@ldiqual Have you made an issue in the Realm-Cocoa repository about this?
No I have not contacted them.
I believe the crash occurs because ObjectMapper defines the <-
functions using the inout
flag. For example:
public func <- <T>(inout left: T, right: Map)
This function is used both for parsing and writing JSON, yet the inout requirement is only necessary when parsing the JSON. Unfortunately I haven't come up with a nice way to split up the mapping functions so that inout
isn't present when writing JSON.
Thanks for your precisions, I've just created and issue on the Realm repository to see if they have any suggestions that could be helpful to solve this problem.
You're correct in the guess that the issue is the spurious inout
flag.
As I said in the Realm issue, I'll go with Mantle because I need a full compatibility with Realm, and solving this inout
flag issue would mean change the design of your library...
Thanks for your support.
I'm also running into this issue. It would be great if ObjectMapper was compatible with Realm.
[edit] Ha, just now saw this comment/solution https://github.com/Hearst-DD/ObjectMapper/issues/294 [/edit]
If anyone runs into this again. We adopted a quick work-around until we move to another solution:
import ObjectMapper
class Event: Object, Mappable {
dynamic var mappingIdentifier = "EVENT_NO_ID" {
didSet {
realmIdentifier = mappingIdentifier
}
}
dynamic var realmIdentifier = "EVENT_NO_ID"
func mapping(map: Map) {
mappingIdentifier <- map["id"]
...
}
override static func primaryKey() -> String? {
return "realmIdentifier"
}
}
We just make sure, that the mapped property isnt the realms primary key.
ugly but in our case so far functional.
Workaround:
func mapping(map: Map) {
var opened = false
if let realm = self.realm where !realm.inWriteTransaction {
realm.beginWrite()
opened = true
}
defer { if opened { map.mappingType == .FromJSON ? try! self.realm?.commitWrite() : self.realm?.cancelWrite() } }
self.name <- map["name"]
// ...
}
It should be used with caution because it might commit implicit changes to your database.
the workaround I use is the following:
class RBase: Object, Mappable {
dynamic var id = NSUUID().UUIDString
dynamic var idz = "" {
didSet {
idz = id
}
}
override class func primaryKey() -> String? {
return "id"
}
func mapping(map: Map) {
....
idz <- map["id"]
idz <- map["idz"]
}
Also see the complete code in Subclassing and Realm #462
I get issues with any Realm backed attribute not just the primary key because the <- operator makes realm think we're modifying the underlying object. Shame, I like objectMapper DSL but for JSON serialising a Realm object it appears to be pretty incompatible without starting a write transaction.
So far I have only seen that problem when when modifying the "id"-property, and we write a new JSON-file at most property changes due to specific requirements, i.e. using two different database engines and using the JSON-file as a "secure" transport mechanism between databases.
I also had this problem just when writing the id (realm object primary key) to json.
My solution bellow:
if map.mappingType == .ToJSON {
var id = self.id
id <- map["id"]
}
else {
id <- map["id"]
}
In this solution you don't need an extra variable and the id will be in your json
Thank you guys, I thought to be alone in this situation!
@thacilima Hello!
I use your code in Swift 3, but I get error libc++abi.dylib: terminating with uncaught exception of type NSException when I call the let JSON = Mapper().toJSON(messageModel)
MessageModel snippet code
override static func primaryKey() -> String? {
return "id"
}
func mapping(map: Map) {
if map.mappingType == .toJSON {
var id = self.id
id <- map["id"]
} else {
id <- map["id"]
}
// id <- map["id"]
date <- map["date"]
message <- map["message"]
imageData <- map["imageData"]
imageURL <- map["imageURL"]
senderID <- map["senderID"]
conversationID <- map["conversationID"]
isIncoming <- map["isIncoming"]
isNew <- map["isNew"]
messageStatus <- map["messageStatus"]
}
required convenience init?(map: Map) {
self.init()
}
Using the clone functionality in Realm, you can make this work
let preferencesCopy: Preferences = Preferences(value: preferences)
preferencesCopy.toJSON()
try using a realm transaction and don't use a primary key
let realm = try! Realm()
try! realm.write({
print("'HERE'",Mapper().toJSONString(user!, prettyPrint: true))
})
i try using this one,it's OK, you can try
https://github.com/APUtils/ObjectMapperAdditions
Most helpful comment
Using the clone functionality in Realm, you can make this work