1

I have created a decoadable struct that will be used to parse data stored in firebase.

struct DiscussionMessage: Decodable {
    var message: String
    var userCountryCode: String
    var userCountryEmoji: String
    var messageTimestamp: Double
    var userName: String
    var userEmailAddress: String
    var fcmToken: String?
    var question: String?
    var recordingUrl: String?
}

I want to use this struct to also store data in firebase. But I get the error:

*** Terminating app due to uncaught exception 'InvalidFirebaseData', reason: '(setValue:) Cannot store object of type __SwiftValue at . Can only store objects of type NSNumber, NSString, NSDictionary, and NSArray.' terminating with uncaught exception of type NSException

When I store the data like this:

let message = DiscussionMessage(message: messageTextView.text, userCountryCode: userCountryCode, userCountryEmoji: userCountryEmoji, messageTimestamp: timestamp, userName: userName, userEmailAddress: userEmail, fcmToken: nil, question: nil, recordingUrl: nil)       
        
messagesReference.childByAutoId().setValue(message)

Is there a way to convert a decodable object to a dictionary so I can store it in firebase?

Leo Dabus
  • 229,809
  • 59
  • 489
  • 571
Parth
  • 2,682
  • 1
  • 20
  • 39
  • https://stackoverflow.com/questions/46597624/can-swift-convert-a-class-struct-data-into-dictionary/46597941#46597941 – Eric Jan 13 '21 at 16:59
  • You can't store nil in the Firebase Real Time Database; nodes with no values don't exist. I would suggest changing the struct to a class, adding a function that returns the values to write as a dictionary - that would also enable you to store default values in the case of nil if you really need the node to exist. – Jay Jan 13 '21 at 21:45
  • 1
    I added an answer that may be a bit shorter of an approach. We're using it in a current project with success but let me know if that works or not. – Jay Jan 15 '21 at 17:26
  • thanks a lot! but I am currently using the answer by Leo. It seemed more robust. – Parth Jan 15 '21 at 17:28

2 Answers2

2

You don't need it to be Decodable. What you need is to be able to encode it (Encodable). So start by declaring your structure as Codable. After encoding it you can convert your data into a dictionary using JSONSerialization jsonObject method:

extension Encodable {
    func data(using encoder: JSONEncoder = .init()) throws -> Data { try encoder.encode(self) }
    func string(using encoder: JSONEncoder = .init()) throws -> String { try data(using: encoder).string! }
    func dictionary(using encoder: JSONEncoder = .init(), options: JSONSerialization.ReadingOptions = []) throws -> [String: Any] {
        try JSONSerialization.jsonObject(with: try data(using: encoder), options: options) as? [String: Any] ?? [:]
    }
}

extension Data {
    func decodedObject<D: Decodable>(using decoder: JSONDecoder = .init()) throws -> D {
        try decoder.decode(D.self, from: self)
    }
}

extension Sequence where Element == UInt8 {
    var string: String? { String(bytes: self, encoding: .utf8) }
}

I would also declare the srtuct properties as constants. If you need to change any value just create a new object:

struct DiscussionMessage: Codable {
    let message, userCountryCode, userCountryEmoji, userName, userEmailAddress: String
    let messageTimestamp: Double
    let fcmToken, question, recordingUrl: String?
}

let message: DiscussionMessage = .init(message: "message", userCountryCode: "BRA", userCountryEmoji: "", userName: "userName", userEmailAddress: "email@address.com", messageTimestamp: 1610557474.227274, fcmToken: "fcmToken", question: "question", recordingUrl: nil)

do {
    let string = try message.string()
    print(string)      // {"fcmToken":"fcmToken","userName":"userName","message":"message","userCountryEmoji":"","userEmailAddress":"email@address.com","question":"question","messageTimestamp":1610557474.2272739,"userCountryCode":"BRA"}
    
    let dictionary = try message.dictionary()
    print(dictionary)  // ["userName": userName, "userEmailAddress": email@address.com, "userCountryEmoji": , "messageTimestamp": 1610557474.227274, "question": question, "message": message, "fcmToken": fcmToken, "userCountryCode": BRA]
    
    let data = try message.data()      // 218 bytes
    let decodedMessages: DiscussionMessage = try data.decodedObject()
    print("decodedMessages", decodedMessages)    // ecodedMessages DiscussionMessage(message: "message", userCountryCode: "BRA", userCountryEmoji: "", userName: "userName", userEmailAddress: "email@address.com", messageTimestamp: 1610557474.227274, fcmToken: Optional("fcmToken"), question: Optional("question"), recordingUrl: nil)
} catch {
    print(error)
}
Leo Dabus
  • 229,809
  • 59
  • 489
  • 571
  • The online answers all use decodable and then use the jsonEncoder to decide snapshot.value from firebase. Can I do that if the struct is of type coadable? – Parth Jan 13 '21 at 17:11
  • `Codable` is just a typealias `typealias Codable = Decodable & Encodable` – Leo Dabus Jan 13 '21 at 17:12
  • Oh so the struct becomes both encodable and decodable? – Parth Jan 13 '21 at 17:13
  • Yes. Btw you can declare your messageTimestamp property as Date and change the dateEncodingStrategy/dateDecodingStrategy to timeInterval since 1970 or whatever you need – Leo Dabus Jan 13 '21 at 17:13
  • Note that if you make your struct conform to `Codable`, there's no need to convert it to a dictionary. Just convert it back and forth to JSON Data, and then convert that JSON Data to a String. – Duncan C Jan 13 '21 at 18:05
  • @DuncanC he needs a dictionary – Leo Dabus Jan 13 '21 at 18:11
  • Well, he *asked* for a dictionary. I'm sensing an A/B question though, since the reason he asked for a dictionary was that he wanted to save the struct into FireBase. Converting to a JSON string would serve that need, and more cleanly. – Duncan C Jan 13 '21 at 18:13
  • @DuncanC how do I make Json from coadable? And how can I parse and convert snapshot to coadable struct? – Parth Jan 14 '21 at 03:44
  • @ParthTamane my updated answer shows the json string and also its data `let data = try message.data()` – Leo Dabus Jan 14 '21 at 04:26
  • Could you also show how I can use JSONDecoder to get back the DiscussionMessage objects from snapshot: [String: Any]? – Parth Jan 14 '21 at 04:50
  • Encodable extension gives there errors `Cannot force unwrap value of non-optional type '(JSONEncoder) throws -> String'`, `Cannot convert return expression of type '(JSONEncoder) throws -> String' to return type 'String'` at func string – Parth Jan 14 '21 at 07:01
  • 1
    nvm the error disappeared after adding all extensions – Parth Jan 14 '21 at 07:09
2

I'm going to toss out an answer and see if it helps.

I would suggest extending Encodable to allow any object that conforms to return a dictionary of itself that can be written to Firebase.

Here's the extension

extension Encodable {
    var dict: [String: Any]? {
        guard let data = try? JSONEncoder().encode(self) else { return nil }
        return (try? JSONSerialization.jsonObject(with: data, options: .allowFragments)).flatMap { $0 as? [String: Any] }
    }
}

and then just a change to the struct

struct DiscussionMessage: Encodable {
   ...your properties...
}

And the code

let msg = DiscussionMessage(data to populate with)
let dict = msg.dict
messagesReference.childByAutoId().setValue(dict)

Keep in mind that the Realtime Database has no nil - any node that has no value does not exist so only the properties that have values will be written.

Oh - and ensure the properties are valid Firebase types of NSNumber, NSString, NSDictionary, and NSArray (and Bool)

Jay
  • 34,438
  • 18
  • 52
  • 81