17

I am trying to decode data from a Firebase DataSnapshot so that it can be decoded using JSONDecoder.

I can decode this data fine when I use a URL to access it with a network request (obtaining a Data object).

However, I want to use the Firebase API to directly obtain the data, using observeSingleEvent as described on this page.

But, when I do this, I cannot seem to convert the result into a Data object, which I need to use JSONDecoder.

Is it possible to do the new style of JSON decoding with a DataSnapshot? How is it possible? I can't seem to figure it out.

wazawoo
  • 545
  • 5
  • 18

7 Answers7

31

I have created a library called CodableFirebase that provides Encoders and Decoders that are designed specifically for Firebase.

So for the example above:

import Firebase
import CodableFirebase

let item: GroceryItem = // here you will create an instance of GroceryItem
let data = try! FirebaseEncoder().encode(item)

Database.database().reference().child("pathToGraceryItem").setValue(data)

And here's how you will read the same data:

Database.database().reference().child("pathToGraceryItem").observeSingleEvent(of: .value, with: { (snapshot) in
    guard let value = snapshot.value else { return }
    do {
        let item = try FirebaseDecoder().decode(GroceryItem.self, from: value)
        print(item)
    } catch let error {
        print(error)
    }
})
Noobass
  • 1,974
  • 24
  • 26
17

I've converted Firebase Snapshots using JSONDecoder by converting snapshots back to JSON in Data format. Your struct needs to conform to Decodable or Codable. I've done this with SwiftyJSON but this example is using JSONSerialization and it still works.

JSONSnapshotPotatoes {
    "name": "Potatoes",
    "price": 5,
}
JSONSnapshotChicken {
    "name": "Chicken",
    "price": 10,
    "onSale": true
}

struct GroceryItem: Decodable {
    var name: String
    var price: Double
    var onSale: Bool? //Use optionals for keys that may or may not exist
}


Database.database().reference().child("grocery_item").observeSingleEvent(of: .value, with: { (snapshot) in
        guard let value = snapshot.value as? [String: Any] else { return }
        do {
            let jsonData = try JSONSerialization.data(withJSONObject: value, options: [])
            let groceryItem = try JSONDecoder().decode(GroceryItem.self, from: jsonData)

            print(groceryItem)
        } catch let error {
            print(error)
        }
    })

Please note that if your JSON keys are not the same as your Decodable struct. You'll need to use CodingKeys. Example:

JSONSnapshotSpinach {
    "title": "Spinach",
    "price": 10,
    "onSale": true
}

struct GroceryItem: Decodable {
    var name: String
    var price: Double
    var onSale: Bool?

    enum CodingKeys: String, CodingKey {
        case name = "title"

        case price
        case onSale
    }
}

You can find more information on this using Apple Docs here.

Errol
  • 278
  • 2
  • 7
6

No. Firebase returns a FIRDataSnapshot that can't be decodable. You can use this structure however, which is pretty simple and easy to understand:

struct GroceryItem {
  
  let key: String
  let name: String
  let addedByUser: String
  let ref: FIRDatabaseReference?
  var completed: Bool
  
  init(name: String, addedByUser: String, completed: Bool, key: String = "") {
    self.key = key
    self.name = name
    self.addedByUser = addedByUser
    self.completed = completed
    self.ref = nil
  }
  
  init(snapshot: FIRDataSnapshot) {
    key = snapshot.key
    let snapshotValue = snapshot.value as! [String: AnyObject]
    name = snapshotValue["name"] as! String
    addedByUser = snapshotValue["addedByUser"] as! String
    completed = snapshotValue["completed"] as! Bool
    ref = snapshot.ref
  }
  
  func toAnyObject() -> Any {
    return [
      "name": name,
      "addedByUser": addedByUser,
      "completed": completed
    ]
  }
  
}

And use toAnyObject() to save your item:

let groceryItemRef = ref.child("items")

groceryItemRef.setValue(groceryItem.toAnyObject())

Source: https://www.raywenderlich.com/139322/firebase-tutorial-getting-started-2

  • 1
    Awesome. This was the direction I was looking for. I didn't realize you could directly work with the snapshot in this way. It was simple to modify for this because I already had a decodable data structure. Also, it turned out that the code for this was even smaller than the original request code! Win win. – wazawoo Nov 25 '17 at 22:10
  • @ricardo what if the node had children as well, how would the `toAnyObject () - > Any` function need to change!? – Learn2Code Sep 16 '20 at 18:47
  • @Learn2Code you’d need ‘toAnyObject() -> Any’ functions inside these children as well, like this: ‘object: yourObject.toAnyObject,’ – Ricardo Daniel Sep 18 '20 at 19:58
  • @RicardoDaniel can you please take a look at the following question https://stackoverflow.com/questions/63911991/issue-reading-firebase-realtime-database-parent-node-with-children-nodes-in-swif. Based on the discussion it would seem I could not just easily do as your answer describes. – Learn2Code Sep 18 '20 at 20:32
4

Or you can use this solution for children

extension DatabaseReference {
  func makeSimpleRequest<U: Decodable>(completion: @escaping (U) -> Void) {
    self.observeSingleEvent(of: .value, with: { snapshot in
        guard let object = snapshot.children.allObjects as? [DataSnapshot] else { return }
        let dict = object.compactMap { $0.value as? [String: Any] }
        do {
            let jsonData = try JSONSerialization.data(withJSONObject: dict, options: [])
            let parsedObjects = try JSONDecoder().decode(U.self, from: jsonData)
            completion(parsedObjects)
        } catch let error {
            print(error)
        }
    })
  }
}

and use

self.refPriceStatistics.child(productId).makeSimpleRequest { (parsedArray: [YourArray]) in
    callback(parsedArray)
}
Leonif
  • 466
  • 4
  • 17
3

If your data type is Codable you can use the following solution to decode directly. You do not need any plugin. I used the solution for Cloud Firestore.

import Firebase
import FirebaseFirestoreSwift


let db = Firestore.firestore()
let query = db.collection("CollectionName")
            .whereField("id", isEqualTo: "123")

guard let documents = snapshot?.documents, error == nil else {
    return
}

if let document = documents.first {
    do {
        let decodedData = try document.data(as: ModelClass.self) 
        // ModelClass a Codable Class

    }
    catch let error {
        // 
    }
}
Mahmud Ahsan
  • 1,755
  • 19
  • 18
2

You can convert the value returned by Firebase to Data, and then decode that.

Add this extension to your project:

extension Collection {
    //Designed for use with Dictionary and Array types
    var jsonData: Data? {
        return try? JSONSerialization.data(withJSONObject: self, options: .prettyPrinted)
    }
}

Then use it to convert the value of the observed snapshot into data, which can then be decoded:

yourRef.observe(.value) { (snapshot) in
    guard snapshot.exists(),
       let value = snapshot.value as? [String],
       let data = value.jsonData else { 
       return
    }
    //cast to expected type
    do {
        let yourNewObject =  try JSONDecoder().decode([YourClass].self, from: data)
    } catch let decodeError {
        print("decodable error")
    }
}
Pranav Kasetti
  • 8,770
  • 2
  • 50
  • 71
vikzilla
  • 3,998
  • 6
  • 36
  • 57
0

You can use this library CodableFirebase or the following extension can be helpful.

extension JSONDecoder {

func decode<T>(_ type: T.Type, from value: Any) throws -> T where T : Decodable {
    do {
        let data = try JSONSerialization.data(withJSONObject: value, options: .prettyPrinted)
        let decoded = try decode(type, from: data)
        return decoded

    } catch {
        throw error
    }
}
manas sharma
  • 463
  • 1
  • 6
  • 8