26

Considering the following model:

class Person: Object {
    dynamic var name = ""
    let hobbies = Dictionary<String, String>()
}

I'm trying to stock in Realm an object of type [String:String] that I got from an Alamofire request but can't since hobbies has to to be defined through let according to RealmSwift Documentation since it is a List<T>/Dictionary<T,U> kind of type.

let hobbiesToStore: [String:String]
// populate hobbiestoStore
let person = Person()
person.hobbies = hobbiesToStore

I also tried to redefine init() but always ended up with a fatal error or else.

How can I simply copy or initialize a Dictionary in RealSwift? Am I missing something trivial here?

gabuchan
  • 785
  • 1
  • 7
  • 18

5 Answers5

37

Dictionary is not supported as property type in Realm. You'd need to introduce a new class, whose objects describe each a key-value-pair and to-many relationship to that as seen below:

class Person: Object {
    dynamic var name = ""
    let hobbies = List<Hobby>()
}

class Hobby: Object {
    dynamic var name = ""
    dynamic var descriptionText = ""
}

For deserialization, you'd need to map your dictionary structure in your JSON to Hobby objects and assign the key and value to the appropriate property.

marius
  • 7,766
  • 18
  • 30
  • Thanks! I've thought of this solution as well (since it's the cleanest one) but it's just really frustrating not to be able to use any Swift structures in RealmSwift... (not even tuples :( ). As my data is really static and simple, I ended up merging the two strings together with a delimiter and created a single `List`. – gabuchan Nov 19 '15 at 13:08
  • There are limitations which prevent us from being able to support any generic Swift structures especially tuples. Among them are that we must be able to figure out the type at runtime and be able to return the value by a dynamic accessor. That doesn't work with tuples. – marius Nov 19 '15 at 13:26
  • **UPDATE 2021**: the `Map` type is now supported. Please see [my answer](https://stackoverflow.com/a/68004936/3780788) below. – Morpheus Jun 16 '21 at 14:46
30

I am currently emulating this by exposing an ignored Dictionary property on my model, backed by a private, persisted NSData which encapsulates a JSON representation of the dictionary:

class Model: Object {
    private dynamic var dictionaryData: NSData?
    var dictionary: [String: String] {
        get {
            guard let dictionaryData = dictionaryData else {
                return [String: String]()
            }
            do {
                let dict = try NSJSONSerialization.JSONObjectWithData(dictionaryData, options: []) as? [String: String]
                return dict!
            } catch {
                return [String: String]()
            }
        }

        set {
            do {
                let data = try NSJSONSerialization.dataWithJSONObject(newValue, options: [])
                dictionaryData = data
            } catch {
                dictionaryData = nil
            }
        }
    }

    override static func ignoredProperties() -> [String] {
        return ["dictionary"]
    }
}

It might not be the most efficient way but it allows me to keep using Unbox to quickly and easily map the incoming JSON data to my local Realm model.

boliva
  • 5,604
  • 6
  • 37
  • 39
  • 1
    Please be aware of the performance impact by the additional JSON (de-)serialization and that you loose the capability to query on the dictionary in that way. – marius Jun 27 '16 at 14:58
  • hi @marius, of course. This is a workaround and, as I said, not the most efficient way to do it, but it works for the cases where I need to have a dictionary reference on my Realm (which I don't really need to query). Hopefully we'll get to see native support for dictionaries at some point, in which case this won't be needed anymore. – boliva Jun 29 '16 at 14:15
6

I would save the dictionary as JSON string in Realm. Then retrive the JSON and convert to dictionary. Use below extensions.

extension String{
func dictionaryValue() -> [String: AnyObject]
{
    if let data = self.data(using: String.Encoding.utf8) {
        do {
            let json = try JSONSerialization.jsonObject(with: data, options: JSONSerialization.ReadingOptions.allowFragments) as? [String: AnyObject]
            return json!

        } catch {
            print("Error converting to JSON")
        }
    }
    return NSDictionary() as! [String : AnyObject]
} }

and

extension NSDictionary{
    func JsonString() -> String
    {
        do{
        let jsonData: Data = try JSONSerialization.data(withJSONObject: self, options: .prettyPrinted)
        return String.init(data: jsonData, encoding: .utf8)!
        }
        catch
        {
            return "error converting"
        }
    }
}
Ansari Awais
  • 299
  • 4
  • 14
5

UPDATE 2021

Since Realm 10.8.0, it is possible to store a dictionary in a Realm object using the Map type.

Example from the official documentation:

class Dog: Object {
    @objc dynamic var name = ""
    @objc dynamic var currentCity = ""
    // Map of city name -> favorite park in that city
    let favoriteParksByCity = Map<String, String>()
}
Morpheus
  • 1,189
  • 2
  • 11
  • 33
  • How Can I decode / encode Map ? I got error when I write Map: ```Type 'QuestionList' does not conform to protocol 'Decodable' || Type 'QuestionList' does not conform to protocol 'Encodable'``` – Ufuk Köşker Aug 31 '21 at 19:53
1

Perhaps a little inefficient, but works for me (example dictionary from Int->String, analogous for your example):

class DictObj: Object {
   var dict : [Int:String] {
      get {
         if _keys.isEmpty {return [:]} // Empty dict = default; change to other if desired
         else {
            var ret : [Int:String] = [:];
            Array(0..<(_keys.count)).map{ ret[_keys[$0].val] = _values[$0].val };
            return ret;
         }
      }
      set {
         _keys.removeAll()
         _values.removeAll()
         _keys.appendContentsOf(newValue.keys.map({ IntObj(value: [$0]) }))
         _values.appendContentsOf(newValue.values.map({ StringObj(value: [$0]) }))
      }
   }
   var _keys = List<IntObj>();
   var _values = List<StringObj>();

   override static func ignoredProperties() -> [String] {
      return ["dict"];
   }
}

Realm can't store a List of Strings/Ints because these aren't objects, so make "fake objects":

class IntObj: Object {
   dynamic var val : Int = 0;
}

class StringObj: Object {
   dynamic var val : String = "";
}

Inspired by another answer here on stack overflow for storing arrays similarly (post is eluding me currently)...

smörkex
  • 336
  • 3
  • 18
  • What you do if you have a value with time Int or Double, etc? The best solution will be to use the Data object and JSONSerialization. – Andrew Kochulab Apr 30 '20 at 15:58