Objectmapper: Calling Mapper().toJSON(object) modifies the underlying object properties

Created on 21 Jan 2015  路  20Comments  路  Source: tristanhimmelman/ObjectMapper

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.

User.swift

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"]
    }
}

ViewController.swift

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

Stack before crash

Make sure you have an All exceptions breakpoint set up.

image

Crash message

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?

wontfix

Most helpful comment

Using the clone functionality in Realm, you can make this work

let preferencesCopy: Preferences = Preferences(value: preferences)
preferencesCopy.toJSON()

All 20 comments

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
image

Was this page helpful?
0 / 5 - 0 ratings